Designing systems features can slot into
/5 min read
Architecture-first beats patching, and the trade I make to get there.
Architecture before screens
Most projects I have rescued went wrong the same way: someone opened a design tool on day one. Screens are seductive because they look like progress, and they commit you to decisions you have not actually made yet. Every dropdown implies a relationship, every list implies an ownership model, and once users have seen a layout it calcifies into a spec. I start somewhere less photogenic. Before any interface exists, I want to know what the entities are, who owns them, and which system is allowed to change which fact.
That work is mostly writing, not code. I sketch the data model until the awkward questions surface: is a call a child of a customer or of a campaign, does an order exist before payment, who wins when the ERP and the webshop disagree about stock. None of these have interface answers. They are decisions about truth, and they are brutally expensive to reverse once data exists in production. Getting them wrong is how systems end up with sync jobs that reconcile two sources that should have been one.
Once the model holds, screens become the easy part. A view is just a projection of data whose shape you already trust, so building one is fast and replacing one is cheap. The tele-sales CRM I built proved this for me: the call queue, the order form, and the customer history were all rewritten at least once, painlessly, because the entities underneath never moved. The interface is the most disposable layer of a system. I design it last for the same reason I paint a house after the walls are up.
The trade you make up front
The honest cost of working this way is a slow first fortnight. While someone else would already be demoing a clickable prototype, I am still drawing boxes and arguing with myself about whether a message thread belongs to a contact or a conversation. Clients feel that silence, and it takes nerve to hold the line. I have learned to be upfront about it: the first weeks buy the months after. It is a trade, not a free lunch, and pretending otherwise is how architecture gets a reputation for being academic.
The payoff shows up the first time a feature lands without a fight. When the CRM needed a fourth messaging channel, the work was an adapter and a week, because a channel was already a first-class concept with a defined contract. Nothing else in the system knew or cared which provider sat behind it. Compare that with the patched alternative, where each new channel would have meant another special case threaded through the queue, the history view, and the reporting. The up-front cost is paid once. The patching cost compounds.
This is not an argument for big design up front. I design the shape, not the features. The goal of the early work is a small set of entities and boundaries that the known roadmap fits inside, plus room for the obvious unknowns, and nothing more. Every abstraction has to justify its existence with at least two real callers, otherwise it is speculation wearing an interface. Less code is better code, and that applies doubly to architecture: the fewer concepts a system has, the more places each new feature can land.
Seams that age well
The seams that survive are the ones cut along the workflow, not the technology. It is tempting to split a system by library or by layer, a services folder here, a queue wrapper there, but those boundaries follow the tools, and tools change. The boundaries I trust follow what people actually do. In the CRM that meant dialling, taking an order, syncing the ERP, and messaging were each their own subsystem, because those are the verbs of an agent's day. Five years from now the verbs will still be the verbs.
A good seam has a one-sentence ownership story. The telephony subsystem owns everything about a call until it ends, then hands a finished record across the boundary, and nobody outside it ever touches a provider API. When I cannot write that sentence for a boundary, the boundary is wrong. The test I apply in review is simple: does this feature live in one subsystem, or is it smeared across three. Smearing is the early symptom of a bad seam, and it shows up long before anyone calls the code legacy.
Seams age well when they are boring. The contract between subsystems in my projects is usually a handful of TypeScript types and a queue or a function call, nothing cleverer. Clever boundaries rot fastest because the next developer cannot hold them in their head, so they tunnel around them, and the seam becomes decoration. I would rather have four plain boundaries everyone respects than one elegant abstraction nobody understands. Respect is the whole point: a seam only protects the system while crossing it properly is easier than cheating.
When to patch anyway
For all of that, I do patch, deliberately and on a schedule. When a feature is an experiment that might be deleted in a month, restructuring the architecture for it is waste. I bolt it on at the edge, keep it out of the core model, and write down what the proper shape would be. The same goes for a hard deadline: shipping a known wart beats missing the date, provided the wart is contained. What I refuse to do is patch the data model itself, because warts in data are permanent.
My rule is that a patch is a loan, and the second patch in the same place is the repayment notice. The first workaround tells you little; the second tells you the architecture has drifted from what the product has become, and the next feature should start with the refactor rather than route around it. Held to that discipline, patching and architecture stop being opposites. The patches mark exactly where the model is under strain, which makes the next round of design work obvious instead of speculative. That feedback loop is the system working.