Skip to content(if available)orjump to list(if available)

How I build software quickly

How I build software quickly

50 comments

·July 14, 2025

shireboy

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.

kukkeliskuu

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.

benterix

> Forget Kubernetes, Redis

While I agree with you, these two are the boring tech of 2025 for me. They work extremely reliably, they have well-defined use cases where they work perfectly and we know very well where they shouldn't be used, we know their gotchas, the interest around them seems to slowly wane. Personally, I'm a huge fan of these, just because they're very stable and they do what they are supposed to do.

creshal

> Always choose extremely boring technology. Just use python/Django/Postgres for everything.

Hell, think twice before you consider postgres. Sqlite scales further than most people would expect it to, especially for local development / spinning up isolated CI instances. And for small apps it tends to be good enough for production too.

Leherenn

I have had some bad experiences with Sqlite for local desktop apps with regards to memory usage, especially on MacOs. Insert and delete a few thousands rows per hour, and over a few days your memory usage has ballooned. It seems to cause a lot of fragmentation.

dkga

Curious to hear more about your experience, since my impression from hacking around in native Apple software is that pretty much a bunch of it is based on top of sqlite3. The Photos app is a case in point I remember off the cuff.

physicsguy

I also like Django a lot. I can get a working project up and running trivially fast.

In my day job I work with Go and while it's fine, I end up writing 10x more code for simple API endpoints and as soon as you add query parameters for filtering, pagination, etc. etc. it gets even longer. Adding a permissions model on top does similar. Of course there's a big performance difference but largely the DB queries dominate performance, even in Python, at least for most of the things I do.

aatd86

oh that's interesting. Is that due to missing libraries in Go? That could be a nice open source project if so.

physicsguy

There's an almost pathological resistance to using anything that might be described as a 'framework' in the Go community in the name of 'simplicity'.

I find such a blanket opinion to be unhelpful, what's fine for writing microservices is less good for bootstrapping a whole SaaS app and I think that people get in a bit too much of an ideological tizz about it all.

kisamoto

Fully agree. I would also say it's easy enough to use Django for (almost) everything for a self contained SaaS startup. Marketing can be done via Wagtail. Support is managed by a reusable app that is a simple static element on every page (similar to Intercom) that redirects to a standard Django page, collects some info about the issue including the user who made it (if authenticated) etc.

I try to simplify the stack further and use SQLite with Borg for backups. Caching leverages Diskcache.

Deployment is slightly more complicated. I use containers and podman with systemd but could easily be a git pull & gunicorn restart.

My frontend practices have gone through some cycles. I found Alpine & HTMX too restrictive to my liking and instead prefer to use Typescript with django-vite integration. Yes it means using some of the frontend tooling but it means I can use TailwindCSS, React, Typescript etc if I want.

GaryNumanVevo

I feel like Django has the largest RoI of any framework out there

physicsguy

I think Rails is stiff competition, it's just I prefer Python.

hobofan

> 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.

bob1029

The gaming use case is what makes this apt advice. If you've got 24h to make a game and you're spending more than ~1h worrying about the source code cleanliness, I don't think it's gonna go well.

Systems like UE blueprints showcase how pointless the pursuit of clean anything is when contrasted with the resulting product experiences.

marginalia_nu

I think scale matters quite a lot here.

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.

rmdashrfv

> 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".

astrobe_

Yes. I wanted to warn about a rough draft being too rough. There are corners one shouldn't cut because this is where the actual problems are. I guess that rally pilots do their recon at a sustained pace, otherwise they might not realize that e.g. the bump there before the corner is vicious.

skrebbel

I think you're talking about unknown unknowns in the tool/framework/library. I think the author is talking about unknown unknowns in the problem space.

rmdashrfv

I was talking about both. Sometimes even in a problem space time constraints demand that you utilize something off the shelf (whether you use part of it or build on top of a custom version of it).

Tools aside, I think everyone who has 10+ years can think of a time they had a prototype go well in a new problem space only to realize during the real implementation that there were still multiple unknown unknowns.

nasretdinov

Yeah, typically when you start thinking something through and actually implementing stuff you can notice that some important part of the behaviour is missing and it might also be something that means that the project is no longer feasible

rmdashrfv

I think this applies to both tools/frameworks/libs and problem spaces

rightbyte

Ye it is something like how making tools just you yourself use is so smooth. Like, they can be full of holes and be a swaying house of cards in general but you still can use them sucessfully.

ChrisMarshallNY

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.

[0] https://github.com/RiftValleySoftware/RVS_Checkbox

[1] https://github.com/RiftValleySoftware/ambiamara

[2] https://github.com/RiftValleySoftware/RVS_Generic_Swift_Tool...

pandemic_region

Tangent, is it a Swift thing to have "* ################################################################## / comment markers ?

It becomes quickly very visually dominant in the source code:

> / ###################################################################################################################################### / // MARK: - PUBLIC BASE CLASS OVERRIDES - / ###################################################################################################################################### */

ChrisMarshallNY

Nope. It's a "Me" thing. I write code that I want to see. I have fairly big files, and it makes it easy to scroll through, quickly. It also displays well, when compiling with docc or Jazzy.

My comment/blank line-to-code ratio is about 50/50. Most of my comments are method/function/property headerdoc/docc labels.

Here's the cloc on the middle project:

    github.com/AlDanial/cloc v 2.04  T=0.03 s (1319.9 files/s, 468842.4 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    Swift                           33           1737           4765           5220
    -------------------------------------------------------------------------------
    SUM:                            33           1737           4765           5220
    -------------------------------------------------------------------------------

paffdragon

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.

gbuk2013

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.

bayindirh

Even as a solo developer, I can swear by decision logs, test and documentation, in that order. I personally keep a "lab notebook" instead of a "decision log" which chronicles the design in real-time, which forms basis of the tests and documentation.

Presence of a lab notebook allows me to write better documentation faster, even if I start late, and tests allow me to verify that the design doesn't drift over time.

Starting blind-mindedly for a one-off tool written in a weekend maybe acceptable, but for anything going to live longer, building the slow foundation allows things built on this foundation to be sound, rational (for the problem at hand) and more importantly understandable/maintainable.

Also, as an unpopular opinion, design on paper first, digitize later.

gbuk2013

Right, an important part of keeping in mind other future developers working on your codebase. You 6 months later is that other developer once the immediate context is gone from your head. :)

bayindirh

That's very true. I like to word this a little differently:

> Six months ago, only I and God knew how this code worked. Now, only God knows. :)

anonzzzies

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.

croes

This will get worse with AI

mattmanser

Work with bad companies, be surprised by poor managers? Who is the "we" in this context, I assume an agency?

So that's not a problem with this process itself. You're describing problems with managers, and problems with developers being unable to handle bad managers.

Even putting aside the manager's incompetence, as a developer you can mitigate this easily in many different ways, here's a few:

- Just don't show it to management

- Deliberately make it obviously broken at certain steps

- Take screen shots of it working and tell people "this is a mockup, I still have to do the hard work of wiring it up"

It's all a balancing act of needing to get feedback from shareholders and managing expectation. If your management is bad, you need to put extra work into managing expectations.

It's like the famous duck story, from Jeff Atwood (see jargon number 4), sometimes you have to manage your managers:

https://blog.codinghorror.com/new-programming-jargon/

anonzzzies

Sure, but we actually thrive here; my company gets called in when systems are not functioning, badly broken, etc and they cannot fix it themselves (usually because the people who built it are gone for decades and they just kept it running with ductape for this time). We never stay for long, we just patch the system and deliver a report. But for figuring out what went wrong and writing the report, we find out how it got to be that way and it's always the same; they suck. Talking banks, hospitals, factories, it really doesn't matter; it's all garbage what gets written and 'TODO: will refactor later' is all over the place. We see many companies from the inside and let me tell you; HN is a lovely echo chamber that resembles nothing in the real world.

whstl

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.

vendiddy

I personally find that doing it well in the first pass slows me down and also ends up in worse overall designs.

But I am also pretty disciplined on the 2nd pass in correcting all of the hacks and rewriting everything that should be rewritten.

There are two problems I have with trying to do it right the first time:

- It's hard to know the intricacies of the requirements upfront without actually implementing the thing, which results in designing an architecture with imperfect knowledge

- It's easy to get stuck in analysis paralysis

FWIW I am a huge fan of John Ousterhout. It may be my all time favorite book on software design.

nasretdinov

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".

albertgoeswoof

> 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