Building software from first principles
Why we start from the problem instead of the stack, what it costs, and why it pays off for the software we build — ours and our clients'.
Most software today is assembled rather than designed. A framework is chosen before anyone has written down what the system is for. A starter template is cloned, a dozen packages are installed “because everyone uses them”, and the actual problem is bent until it fits the shape of the tools.
It works. For a while it works very well — the first demo arrives in days, not weeks. The trouble shows up later, quietly, in the cost of every change after the first.
This note is about the other way of doing it, the one we use for our own products and for the custom software we build for clients: starting from the problem, not from the stack.
What “first principles” actually means
The phrase gets used loosely, so here is what we mean by it in practice. Reasoning from first principles means breaking a problem down to the things you know are true — the constraints that cannot be negotiated — and building up from there, instead of reasoning by analogy (“company X uses this stack, so should we”).
In software, analogy is the default. It is not a bad instinct: other people’s choices encode a lot of hard-won experience. But they also encode their constraints, their team size, their traffic, their history. Copying the solution without the constraints means inheriting complexity you never needed.
Start with the constraint, not the tool
Before writing a line of code we try to answer three questions, in writing, as plainly as possible:
- What actually has to be true for this to work? Not features — invariants. “An invoice can never be issued twice.” “A user only ever sees their own data.” “The report must be ready by 7:00 every morning.”
- What is expensive — in time, money or attention — and why? Sometimes it is computation. More often it is people: the hours spent reconciling spreadsheets, the context switches, the manual checks nobody trusts anymore.
- What can we remove without losing anything that matters? Every feature, screen and integration is examined for whether it serves one of the answers above.
The answers almost always point to a smaller, sharper system than the one we would have built by default. Only then do we choose tools — and we choose them to fit the constraints, not the other way round.
Every dependency is a promise
A modern project can easily pull in hundreds of packages before it does anything useful. Each one is a promise someone else has to keep: that it will stay maintained, stay secure, stay compatible with everything else, and keep behaving the way it does today.
Fig. 1 — The same problem, two dependency graphs. Each line on the left is something that can break, change licence or need patching.
We are not dogmatic about this. We use good libraries constantly — writing your own cryptography or database driver is a mistake. The rule is simpler: a dependency has to earn its place. If it saves more than it costs over the life of the system, it stays. If it is there because a template put it there, it goes.
In practice this means:
- Fewer layers. Each abstraction is a place where bugs hide and where new people get lost. Three well-understood layers beat fourteen thin ones.
- Boring choices where it doesn’t matter. A relational database, plain HTTP, server-rendered pages where they suffice. Novelty is spent only where the problem genuinely demands it.
- Explicit boundaries. The core logic of the system does not know which framework is rendering it. That makes it testable today and replaceable tomorrow.
The cost curve nobody plans for
The real argument for building this way is not elegance. It is cost — specifically, the cost of the next change.
Fig. 2 — Illustrative. A designed system usually costs a little more up front, and much less every month after.
Assembled systems are cheap at the start because most of the work has been borrowed. But every borrowed decision comes with assumptions, and as the product grows those assumptions start to collide with reality. Changes that should take an afternoon take a week. Upgrades get postponed because nobody is sure what they will break. Eventually someone proposes a rewrite.
A system designed around its constraints usually takes a little longer to get to the first demo. After that, the curve flattens: new features slot into a structure that was built to receive them, and the people maintaining it can still hold the whole thing in their heads.
Small systems age well
There is a quieter benefit, and it is the one we care about most. Software built this way is legible. A new engineer — yours or ours — can read it, understand why it is shaped the way it is, and change it with confidence. That matters far more than any particular technology, because the people who maintain a system in three years are rarely the people who wrote it.
It is also how we think about handover on client projects. The goal is not to make you depend on us. It is to leave you with something small enough to understand, documented enough to trust, and solid enough that you rarely need to call.
How this shows up in our work
When we start a custom project, the first deliverable is not code. It is a short document: the invariants, the expensive parts, what we propose to leave out, and the architecture that follows from all of that. It is usually a few pages long, and it is the most valuable thing we produce, because every later decision can be checked against it.
Then we build — in small, working increments, each one deployed and usable, so you see the system take shape instead of waiting for a big reveal.
If you have a problem that off-the-shelf tools keep almost solving, tell us about it. Starting from the problem is what we do.