We encounter many rough drafts (yours) in production systems. If the original devs are still there, it is usually something along the lines of: I showed the rough draft to my manager, they flagged is as done and I was assigned to another task.
An important dimension that is not really touched upon in the article is development speed over time. This will decrease with time, project and team size. Minimising the reduction rate may require doing things things that slow down immediate development for the sake of longer term velocity. Some examples would be test, documentation, decision logs, Agile ceremonies etc.
Some omissions during initial development may have a very long tail of negative impact - obvious examples are not wiring in observability into the code from the outset, or not structuring code with easy testing being an explicit goal.
This is why I have grown to appreciate gradual typing, at least for solo projects. In Python-land I can just riff over a few functions/scripts until I get a rough idea of the APIs/workflows I want, then bring mypy into the mix and shape things into their "final" form (this takes maybe a few hours away). Rinse repeat for each new feature, but at every iteration you build up from a "nicely-typed" foundation.
Sometimes a redesign of the types you relied on becomes necessary to accommodate new stuff, but that would be true under any language; otoh, the "exploratory" part of coding feels faster and easier.
The initial rough draft almost reminds me of the old "Build One to Throw Away" approach, which I think is pretty nice - not getting caught up in making something production ready, but rather exploring the problem space first.
I do admit that modern frameworks also help in that regard, instead of just stitching libraries together, at least for typical webdev stuff instead of more minimalistic CLI utilities or tools. The likes of Ruby on Rails, Django, Laravel, Express and even the likes of ASP.NET or Spring Boot.
> It can reveal “unknown unknowns”. Often, prototypes uncover things I couldn’t have anticipated.
This is the exact opposite of my experience. Every time I am playing around with something, I feel like I'm experiencing all of its good and none of its bad ... a honeymoon phase if you will.
It's not until I need to cover edge cases and prevent all invalid state and display helpful error messages to the user, and eliminate any potential side effects that I discover the "unknown unknowns".
One important aspect, also highlighted by others, is that for the long term you actually _don't_ want to focus solely on the immediate task you're solving. Sure, short term the tasks are getting done quicker, but since the end goal typically is implementing a full coherent solution you _have_ to step back and take a look at a bigger picture every now and then. Typically you won't be allocated specific time when to do this, so this "take a bird's eye view" part has to be incorporated into day-to-day work instead. It's also typically easier to notice bigger issues while you're already in the trenches, compared to doing "cleanup" separately "later".
This post resonates deeply with how I build products, especially in the era of LLMs and AI-assisted coding.
I usually start top-down, sketching the API surface or UI scaffold before diving into real logic. Iteration drives the process: get something running, feel out the edges, and refine.
I favor MVPs that work end-to-end, to validate flow and reduce risk early. That rhythm helps me ship production-ready software quickly, especially when navigating uncertainty.
One recent WIP: https://zero-to-creator.netlify.app/. I built it for my kid, but I’m evolving it into a full-blown product by tweaking the edges as I go.
> Data modeling is usually important to get right, even if it takes a little longer. Making invalid states unrepresentable can prevent whole classes of bugs. Getting a database schema wrong can cause all sorts of headaches later
So much this.
Get the data model right before you go live, and everything is so simple, get it wrong and be prepared for constant pain balancing real data, migrations, uptime and new features. Ask me how I know
If you're building something yourself or in a small team, I absolutely agree with everything written in the post. In fact, I'd emphasize you should lean into this sort of quick and dirty development methodology in such a context, because this is the strength of small scale development. Done correctly it will have you running circles around larger operations. Bugs are almost always easy to fix later for a small team or solo dev operation as you can expect everyone involved to have a nearly perfect mental model of the entire project, and the code itself will regardless of the messes you make tend to keep relatively simple due to Conway's law.
In larger development projects, fixing bugs and especially architectural mistakes is exponentially more expensive as code understanding is piecemeal, the architecture is inevitably nightmarishly complex (Conway again), and large scale refactoring means locking down parts of the code base so that dozens to hundreds of people can't do anything (which means it basically never happens). In such a setting the overarching focus is and should be on correctness at all steps. The economies of scale will still move things forward at an acceptable pace, even if individual developers aren't particularly productive working in such a fashion.
Fast builds are important. I've been doing server side stuff for a few decades and there are some things you can do to turn slow builds into fast builds. I mostly work on the JVM but a lot of this stuff ports well to other stacks (e.g. Ruby or python).
Basically there are things you can't avoid that are not necessarily fast (e.g. compilation, docker build, etc.) and things that you can actually control and optimize. Tests and integration tests are part of that. Learning how to write good effective tests that are quick to run is important. Because you might end up with hundreds of those and you'll be spending a lot of your career waiting for those to run. Over and over again.
Here's what I do:
- I run integration tests concurrently. My CPUs max out when I run my tests. My current build runs around 400 integration tests in about 35 seconds. Integration test means the tests are proper black box tests that hit a REST API with my server talking to a DB, Elasticsearch and Redis. Each test might require users/teams and some content set up. We're talking many thousands of API calls happening in about 35 seconds.
- There is no database cleanup in between tests. Database cleanup is slow. Each build starts with an ephemeral docker container. So it starts empty but by the time the build is over you have a pretty full database.
- To avoid test interaction, all data is randomized. I use a library that generates human readable names, email addresses, etc. Creating new users/teams is fast, recreating the database schema isn't. And because at any time there can be 10 separate tests running, you don't want this anyway. Some tests share the same read only test fixture and team. Recreating the same database content over and over again is stupid.
- A proper integration test is a scenario that is representative of what happens in your real system. It's not a unit test. So the more side effects, the better. Your goal is to find anything that might break when you put things together. Finding weird feature interactions, performance bottlenecks, and sources of flakiness is a goal here and not something you are trying to avoid. Real users don't use an empty system. And they won't have it exclusive to themselves either. So having dozens of tests running at the same time adds realism.
- Unit tests and integration tests have different goals. With integration tests you want to cover features, not code. Use unit tests for code coverage. The more features an integration test touches, the better. There is a combinatorial explosion of different combinations of inputs. It's mathematically impossible to test all of them with an integration test. So, instead of having more integration tests, write better scenarios for your tests. Add to them. Refine them with detail. Asserting stuff is cheap. Setting things up isn't. Make the most of what you setup.
- IMHO anything in between scenario tests and unit tests is a waste of time. I hate white box tests. Because they are expensive to run and write and yet not as valuable as a good blackbox integration test. Sometimes you have to. But these are low value, high maintenance, expensive to run tests. A proper unit tests is high value, low maintenance and very fast to run (it mocks/stubs everything it needs, there is no setup cost). A proper integration tests is high value, low maintenance, and slow to run. You justify the time investment with value. Low maintenance here means not a lot of code is needed to set things up.
- Your integration test becomes a load and stress test as well. Many teams don't bother with this. I run mine 20 times a day. Because it only takes less than a minute. Anything that increases that build time, gets identified and dealt with. My tests passing gives me a high degree of certainty that nothing important has broken.
- Most of the work creating a good test is setting up the given part of a BDD style test. Making that easy with some helper functions is key. Most of my tests require users, te...
In recent years, I have learned how to build sufficiently robust systems fast.
Here are some things I have learned:
* Learn one tool well. It is often better to use a tool that you know really well than something that on the surface seems to be more appropriate for the problem. For extremely large number of real-life problems, Django hits the sweet spot.
Several times I have started a project thinking that maybe Django is too heavy, but soon the project outgrew the initial idea. For example, I just created a status page app. It started as a single file Django app, but luckily realized soon that it makes no sense to go around Djangos limitations.
* In most applications that fit the Django model, data model is at the center of everything. Even if making a rought prototype, never postpone data model refactoring. It just becomes more and more expensive and difficult to change over time.
* Most applications don't need to be single-page apps nor require heavy frontend frameworks. Even for those that can benefit from it, traditional Django views is just fine for 80% of the pages. For the rest, consider AlpineHJS/HTMX
* Most of the time, it is easier to build the stuff yourself. Need to store and edit customers? With Django, you can develop simple a CRM app inside your app in just few hours. Integrating commercial CRM takes much more time. This applies to everything: status page, CRM, support system, sales processes, etc. as well as most Django apps/libraries.
* Always choose extremely boring technology. Just use python/Django/Postgres for everything. Forget Kubernetes, Redis, RabbitMQ, Celery, etc. Alpine/HTMX is an exception, because you can avoid much of the Javascript stack.
Your first point resonates. I had an idea I wanted to explore and decided to use Make.com and google sheets. After two hours I said, screw this, and spun up my entire idea in a Rails app in 30 minutes.
Knowing a good Swiss army tool very well is a super power.
I think HTMX as "simple" solutions comes into the same class as you mention "django being too heavy".
Sure, Django,Rails,Asp MVC,etc with static pages could be fully functional for something that is 99% CRUD.
But I myself foolishly tried using static view+HTMX for something that should have been built with something more dynamic like Vue/React(perhaps Alpine?) from the start due to actual client-side complexity.
In general imho one should classify what your main interaction points are and use the correct tools for each.
- Is it static pages for masses of consumers with little to no interaction? (Ie a news-site,etc) then fast SEO-friendly pages is your nr 1 priority.
- Is it mainly massive CRUD updates that needs persistence, go with server built pages with Django, Rails, etc.
- Is it an actual client/data heavy UI (games but also scenarios where you want fast client data slicing or processes/sources that complete at different paces), use an SPA.
Who could argue with the "right tool for the job"? I am not saying that there are no use cases for other approaches.
But when you say you had a "static view" (in singular, not plural) with HTMX, what do you mean? I am very interested in hearing what kind of complex interaction you had where that approach failed?
With HTMX, I have found the most useful approach is to build your pages from lots of very small components. Initially, I don't worry about reuse of those components, it is more about decomposition than reuse.
Of course, if you want to do "fast client data slicing", then HTMX makes no sense, because it pushes logic to the backend and by definition your solution wants to process data in the fronted. So if you start from that premise, HTMX is obviously the wrong tool.
If you want to use HTMX, you need to rethink your solution, i.e. question whether you really need "fast client data slicing". So why bring all the data to the frontend, and not just the slice you want to visualize?
One reasonable use case is reducing server load. I have one app where data is loaded from static generated pages (periodically updated, for performance, stability and reduced server load), and sorting is done on the frontend using Javascript, based on data attributes on the DOM.
I actually try to build it "well" in the first pass, even for prototyping. I'm not gonna say I succeed but at least I try.
This doesn't mean writing tests for everything, and sometimes it means not writing tests at all, but it means that I do my best to make code "testable". It shouldn't take more time to do this, though: if you're making more classes to make it testable, you're already messing it up.
This also doesn't mean compromising in readability, but it does mean eschewing practices like "Clean Code". Functions end up being as large as they need to be. I find that a lot of people doing especially Ruby and Java tend to spend too much time here. IMO having lots of 5-line functions is totally unnecessary, so I just skip this step altogether.
It also doesn't mean compromising on abstractions. I don't even like the "rule of three" because it forces more work down the line. But since I prefer DEEP classes and SMALL interfaces, in the style of John Ousterhout, the code doesn't really take longer to write. It does require some thinking but it's nothing out of the ordinary at all. It's just things that people don't do out of inertia.
One thing I am a bit of hardliner about is scope. If the scope is too large, it's probably not prototype or MVP material, and I will fight to reduce it.
EDIT: kukkeliskuu said below "learn one tool well". This is also key. Don't go "against the grain" when writing prototypes or first passes. If you're fighting the framework, you're on the wrong path IME.
I've found that a "rough draft" is pretty hard to maintain as a "draft," when you have a typical tech manager.
Instead, it becomes "final ship" code.
I tend to write ship code from the start, but do so, in a manner that allows a lot of flexibility. I've learned to write "ship everywhere," even my test harnesses tend to be fairly robust, ship-Quality apps.
A big part of that, is very high-Quality modules. There's always stuff that we know won't change, or, if so, a change is a fairly big deal, so we sequester those parts into standalone modules, and import them as dependencies.
Here's an example of one that I just finished revamping[0]. I use it in this app[1], in the settings popover. I also have this[2] as a baseline dependency that I import into almost everything.
It can make it really fast, to develop a new application, and can keep the Quality pretty high, even when that's not a principal objective.
This is very familiar. Rough draft, some manual execution often wrapped in a unit test executor, or even written in a different scripting language just to verify the idea. This often helped me to show that we don't even want to build the thing, because it won't work the way people want it to.
The part about distraction in code feels also very real. I am really prone to "clean up things", then realize I'm getting into a rabbit hole and my change grows to a size that my mates won't be happy reviewing. These endeavors often end with complete discard to get back on track and keep the main thing small and focused - frequent small local commits help a lot here. Sometimes I manage to salvage something and publish in a different PR when time allows it.
Business mostly wants the result fast and does not understand tradeoffs in code until the debt hits the size of a mountain that makes even trivial changes painfully slow. But it's about balance, which might be different on different projects.
Small, focused, simple changes definitely help. Although, people are not always good at slicing a larger solution into smaller chunks. I sometimes see commits that ship completely unused code unrelated to anything with a comment that this will be part of some future work...then prio shifts, people come and go, and a year later we have to throw out all of that, because it does not apply to the current state and no one knows anymore what was the plan with that.
> For example, if you’re making a game for a 24-hour game jam, you probably don’t want to prioritize clean code. That would be a waste of time! Who really cares if your code is elegant and bug-free?
Hate to be an anecdote Andy here, but as someone who has done a lot of code review at (non-game) hackathons in the past (primarily to prevent cheating), the teams that performed the best were also usually the ones with the best code quality and often at least some rudimentary testing setup.
This pretty much exactly describes my strategy to ship better code faster. Especially the “top down” approach: I’m actually kind of surprised there isn’t like a “UI first” or “UI Driven Development” manifesto like w TDD or BDD. Putting a non functional UI in front of stakeholders quickly often results in better requirements gathering and early refinement that would be more costly later in the cycle.
I think at that point it's almost better to come with paper printouts for 2 reasons.
1: Tacticale/shareable/paintable in a physical meeting
2: It drives home that it's a sketch, with bad customers a visible UI so easily hides the enormous amounts of complexity that can sometimes be under a "simple" UI and make it hard to educate them as to why the UI sketch one did in an hour or two then needs 1500 hours of engineering to become a functioning system.
"Learn to walk before you run. I firmly believe in this wisdom. First build a foundational model, then tackle boundary challenges and efficiency optimization."
> What is my team’s idea of “good enough”? What bugs are acceptable, if any? Where can I do a less-than-perfect job if it means getting things done sooner?
I hop between projects regularly, and this has been the biggest source of inter-team conflict in my career.
Different people from different backgrounds have an assumed level of what "good enough". The person from big tech is frustrated because no one else is testing thoroughly. The person from a startup is frustrated because everyone else is moving too slow.
It would be nice if the "good enough" could be made explicit so teams are on the same page.
i often use c# and Visual Studio to write prototype code. C# can be used in a C like syntay (my destination language) and has much better turnaround times
> For example, if you’re making a game for a 24-hour game jam, you probably don’t want to prioritize clean code. That would be a waste of time! Who really cares if your code is elegant and bug-free?
Having worked on some 24-hour game jams and similar, I've found completely the opposite. It's when you're in a real hurry that you really can't afford bad code. Writing better code will make it easier to get it right, will put less pressure on my working memory, will let me try things faster and make those last-minute changes I wanted, will make adding features towards the end easier rather than harder and, crucially, will both reduce the chance that I need to do intense debugging and make debugging I need to do easier.
Working with good code just feels lighter.
The thing that breaks 24-hour projects isn't writing code too slowly, it's writing yourself into a corner or hitting some problem that derails your work, takes hours to solve or doesn't even get resolved until after the deadline.
A game jam isn't the place to try to squish all bugs, sure, but that's a question of what you're doing, not how. I still want to write good code in that situation because it makes my life easier within the time constraints, and because, even if I'm fine with some bugs, there are still lots of bugs that render a game unpleasant or unplayable.
I'll need to fix some bugs no matter what; I'd rather fix fewer, easier bugs than more, harder bugs!
The same thing applies to longer time horizons too. When you have more time you have more slack to deal with bad code, but that doesn't mean it makes any more sense to write it!
And, of course, once you get in the right habits, writing solid quality code becomes basically free... but, even if it really did meaningfully slow you down, chances are it would still be worth doing in expectation.
40 comments
[ 2.5 ms ] story [ 58.3 ms ] threadThe author said LLM helps. Let's lynch him!
Some omissions during initial development may have a very long tail of negative impact - obvious examples are not wiring in observability into the code from the outset, or not structuring code with easy testing being an explicit goal.
Sometimes a redesign of the types you relied on becomes necessary to accommodate new stuff, but that would be true under any language; otoh, the "exploratory" part of coding feels faster and easier.
I do admit that modern frameworks also help in that regard, instead of just stitching libraries together, at least for typical webdev stuff instead of more minimalistic CLI utilities or tools. The likes of Ruby on Rails, Django, Laravel, Express and even the likes of ASP.NET or Spring Boot.
This is the exact opposite of my experience. Every time I am playing around with something, I feel like I'm experiencing all of its good and none of its bad ... a honeymoon phase if you will.
It's not until I need to cover edge cases and prevent all invalid state and display helpful error messages to the user, and eliminate any potential side effects that I discover the "unknown unknowns".
I usually start top-down, sketching the API surface or UI scaffold before diving into real logic. Iteration drives the process: get something running, feel out the edges, and refine.
I favor MVPs that work end-to-end, to validate flow and reduce risk early. That rhythm helps me ship production-ready software quickly, especially when navigating uncertainty.
One recent WIP: https://zero-to-creator.netlify.app/. I built it for my kid, but I’m evolving it into a full-blown product by tweaking the edges as I go.
So much this.
Get the data model right before you go live, and everything is so simple, get it wrong and be prepared for constant pain balancing real data, migrations, uptime and new features. Ask me how I know
If you're building something yourself or in a small team, I absolutely agree with everything written in the post. In fact, I'd emphasize you should lean into this sort of quick and dirty development methodology in such a context, because this is the strength of small scale development. Done correctly it will have you running circles around larger operations. Bugs are almost always easy to fix later for a small team or solo dev operation as you can expect everyone involved to have a nearly perfect mental model of the entire project, and the code itself will regardless of the messes you make tend to keep relatively simple due to Conway's law.
In larger development projects, fixing bugs and especially architectural mistakes is exponentially more expensive as code understanding is piecemeal, the architecture is inevitably nightmarishly complex (Conway again), and large scale refactoring means locking down parts of the code base so that dozens to hundreds of people can't do anything (which means it basically never happens). In such a setting the overarching focus is and should be on correctness at all steps. The economies of scale will still move things forward at an acceptable pace, even if individual developers aren't particularly productive working in such a fashion.
Basically there are things you can't avoid that are not necessarily fast (e.g. compilation, docker build, etc.) and things that you can actually control and optimize. Tests and integration tests are part of that. Learning how to write good effective tests that are quick to run is important. Because you might end up with hundreds of those and you'll be spending a lot of your career waiting for those to run. Over and over again.
Here's what I do:
- I run integration tests concurrently. My CPUs max out when I run my tests. My current build runs around 400 integration tests in about 35 seconds. Integration test means the tests are proper black box tests that hit a REST API with my server talking to a DB, Elasticsearch and Redis. Each test might require users/teams and some content set up. We're talking many thousands of API calls happening in about 35 seconds.
- There is no database cleanup in between tests. Database cleanup is slow. Each build starts with an ephemeral docker container. So it starts empty but by the time the build is over you have a pretty full database.
- To avoid test interaction, all data is randomized. I use a library that generates human readable names, email addresses, etc. Creating new users/teams is fast, recreating the database schema isn't. And because at any time there can be 10 separate tests running, you don't want this anyway. Some tests share the same read only test fixture and team. Recreating the same database content over and over again is stupid.
- A proper integration test is a scenario that is representative of what happens in your real system. It's not a unit test. So the more side effects, the better. Your goal is to find anything that might break when you put things together. Finding weird feature interactions, performance bottlenecks, and sources of flakiness is a goal here and not something you are trying to avoid. Real users don't use an empty system. And they won't have it exclusive to themselves either. So having dozens of tests running at the same time adds realism.
- Unit tests and integration tests have different goals. With integration tests you want to cover features, not code. Use unit tests for code coverage. The more features an integration test touches, the better. There is a combinatorial explosion of different combinations of inputs. It's mathematically impossible to test all of them with an integration test. So, instead of having more integration tests, write better scenarios for your tests. Add to them. Refine them with detail. Asserting stuff is cheap. Setting things up isn't. Make the most of what you setup.
- IMHO anything in between scenario tests and unit tests is a waste of time. I hate white box tests. Because they are expensive to run and write and yet not as valuable as a good blackbox integration test. Sometimes you have to. But these are low value, high maintenance, expensive to run tests. A proper unit tests is high value, low maintenance and very fast to run (it mocks/stubs everything it needs, there is no setup cost). A proper integration tests is high value, low maintenance, and slow to run. You justify the time investment with value. Low maintenance here means not a lot of code is needed to set things up.
- Your integration test becomes a load and stress test as well. Many teams don't bother with this. I run mine 20 times a day. Because it only takes less than a minute. Anything that increases that build time, gets identified and dealt with. My tests passing gives me a high degree of certainty that nothing important has broken.
- Most of the work creating a good test is setting up the given part of a BDD style test. Making that easy with some helper functions is key. Most of my tests require users, te...
Here are some things I have learned:
* Learn one tool well. It is often better to use a tool that you know really well than something that on the surface seems to be more appropriate for the problem. For extremely large number of real-life problems, Django hits the sweet spot.
Several times I have started a project thinking that maybe Django is too heavy, but soon the project outgrew the initial idea. For example, I just created a status page app. It started as a single file Django app, but luckily realized soon that it makes no sense to go around Djangos limitations.
* In most applications that fit the Django model, data model is at the center of everything. Even if making a rought prototype, never postpone data model refactoring. It just becomes more and more expensive and difficult to change over time.
* Most applications don't need to be single-page apps nor require heavy frontend frameworks. Even for those that can benefit from it, traditional Django views is just fine for 80% of the pages. For the rest, consider AlpineHJS/HTMX
* Most of the time, it is easier to build the stuff yourself. Need to store and edit customers? With Django, you can develop simple a CRM app inside your app in just few hours. Integrating commercial CRM takes much more time. This applies to everything: status page, CRM, support system, sales processes, etc. as well as most Django apps/libraries.
* Always choose extremely boring technology. Just use python/Django/Postgres for everything. Forget Kubernetes, Redis, RabbitMQ, Celery, etc. Alpine/HTMX is an exception, because you can avoid much of the Javascript stack.
Knowing a good Swiss army tool very well is a super power.
Sure, Django,Rails,Asp MVC,etc with static pages could be fully functional for something that is 99% CRUD.
But I myself foolishly tried using static view+HTMX for something that should have been built with something more dynamic like Vue/React(perhaps Alpine?) from the start due to actual client-side complexity.
In general imho one should classify what your main interaction points are and use the correct tools for each.
- Is it static pages for masses of consumers with little to no interaction? (Ie a news-site,etc) then fast SEO-friendly pages is your nr 1 priority.
- Is it mainly massive CRUD updates that needs persistence, go with server built pages with Django, Rails, etc.
- Is it an actual client/data heavy UI (games but also scenarios where you want fast client data slicing or processes/sources that complete at different paces), use an SPA.
Ie, right tool for the right job.
But when you say you had a "static view" (in singular, not plural) with HTMX, what do you mean? I am very interested in hearing what kind of complex interaction you had where that approach failed?
With HTMX, I have found the most useful approach is to build your pages from lots of very small components. Initially, I don't worry about reuse of those components, it is more about decomposition than reuse.
Of course, if you want to do "fast client data slicing", then HTMX makes no sense, because it pushes logic to the backend and by definition your solution wants to process data in the fronted. So if you start from that premise, HTMX is obviously the wrong tool.
If you want to use HTMX, you need to rethink your solution, i.e. question whether you really need "fast client data slicing". So why bring all the data to the frontend, and not just the slice you want to visualize?
One reasonable use case is reducing server load. I have one app where data is loaded from static generated pages (periodically updated, for performance, stability and reduced server load), and sorting is done on the frontend using Javascript, based on data attributes on the DOM.
That works well until that tool stops being maintained, or is evolving in such an odd direction that it breaks your code.
Django is really an exception to something one can rely on, but reality is more like Angular -> breaking, Vue -> breaking, React -> breaking.
This doesn't mean writing tests for everything, and sometimes it means not writing tests at all, but it means that I do my best to make code "testable". It shouldn't take more time to do this, though: if you're making more classes to make it testable, you're already messing it up.
This also doesn't mean compromising in readability, but it does mean eschewing practices like "Clean Code". Functions end up being as large as they need to be. I find that a lot of people doing especially Ruby and Java tend to spend too much time here. IMO having lots of 5-line functions is totally unnecessary, so I just skip this step altogether.
It also doesn't mean compromising on abstractions. I don't even like the "rule of three" because it forces more work down the line. But since I prefer DEEP classes and SMALL interfaces, in the style of John Ousterhout, the code doesn't really take longer to write. It does require some thinking but it's nothing out of the ordinary at all. It's just things that people don't do out of inertia.
One thing I am a bit of hardliner about is scope. If the scope is too large, it's probably not prototype or MVP material, and I will fight to reduce it.
EDIT: kukkeliskuu said below "learn one tool well". This is also key. Don't go "against the grain" when writing prototypes or first passes. If you're fighting the framework, you're on the wrong path IME.
Instead, it becomes "final ship" code.
I tend to write ship code from the start, but do so, in a manner that allows a lot of flexibility. I've learned to write "ship everywhere," even my test harnesses tend to be fairly robust, ship-Quality apps.
A big part of that, is very high-Quality modules. There's always stuff that we know won't change, or, if so, a change is a fairly big deal, so we sequester those parts into standalone modules, and import them as dependencies.
Here's an example of one that I just finished revamping[0]. I use it in this app[1], in the settings popover. I also have this[2] as a baseline dependency that I import into almost everything.
It can make it really fast, to develop a new application, and can keep the Quality pretty high, even when that's not a principal objective.
[0] https://github.com/RiftValleySoftware/RVS_Checkbox
[1] https://github.com/RiftValleySoftware/ambiamara
[2] https://github.com/RiftValleySoftware/RVS_Generic_Swift_Tool...
The part about distraction in code feels also very real. I am really prone to "clean up things", then realize I'm getting into a rabbit hole and my change grows to a size that my mates won't be happy reviewing. These endeavors often end with complete discard to get back on track and keep the main thing small and focused - frequent small local commits help a lot here. Sometimes I manage to salvage something and publish in a different PR when time allows it.
Business mostly wants the result fast and does not understand tradeoffs in code until the debt hits the size of a mountain that makes even trivial changes painfully slow. But it's about balance, which might be different on different projects.
Small, focused, simple changes definitely help. Although, people are not always good at slicing a larger solution into smaller chunks. I sometimes see commits that ship completely unused code unrelated to anything with a comment that this will be part of some future work...then prio shifts, people come and go, and a year later we have to throw out all of that, because it does not apply to the current state and no one knows anymore what was the plan with that.
It helps reveal unknowns in the problem space that synthetic data might miss.
Hate to be an anecdote Andy here, but as someone who has done a lot of code review at (non-game) hackathons in the past (primarily to prevent cheating), the teams that performed the best were also usually the ones with the best code quality and often at least some rudimentary testing setup.
1: Tacticale/shareable/paintable in a physical meeting
2: It drives home that it's a sketch, with bad customers a visible UI so easily hides the enormous amounts of complexity that can sometimes be under a "simple" UI and make it hard to educate them as to why the UI sketch one did in an hour or two then needs 1500 hours of engineering to become a functioning system.
Lightwork
I hop between projects regularly, and this has been the biggest source of inter-team conflict in my career.
Different people from different backgrounds have an assumed level of what "good enough". The person from big tech is frustrated because no one else is testing thoroughly. The person from a startup is frustrated because everyone else is moving too slow.
It would be nice if the "good enough" could be made explicit so teams are on the same page.
Having worked on some 24-hour game jams and similar, I've found completely the opposite. It's when you're in a real hurry that you really can't afford bad code. Writing better code will make it easier to get it right, will put less pressure on my working memory, will let me try things faster and make those last-minute changes I wanted, will make adding features towards the end easier rather than harder and, crucially, will both reduce the chance that I need to do intense debugging and make debugging I need to do easier.
Working with good code just feels lighter.
The thing that breaks 24-hour projects isn't writing code too slowly, it's writing yourself into a corner or hitting some problem that derails your work, takes hours to solve or doesn't even get resolved until after the deadline.
A game jam isn't the place to try to squish all bugs, sure, but that's a question of what you're doing, not how. I still want to write good code in that situation because it makes my life easier within the time constraints, and because, even if I'm fine with some bugs, there are still lots of bugs that render a game unpleasant or unplayable.
I'll need to fix some bugs no matter what; I'd rather fix fewer, easier bugs than more, harder bugs!
The same thing applies to longer time horizons too. When you have more time you have more slack to deal with bad code, but that doesn't mean it makes any more sense to write it!
And, of course, once you get in the right habits, writing solid quality code becomes basically free... but, even if it really did meaningfully slow you down, chances are it would still be worth doing in expectation.
Sucks, as that is effectively arguing for rote repetition in the tasks. And it is. But, that works. Really well.
Stated differently, show me someone that can write clean code in a hurry, and you have shown me someone that has written this before.