Skip to content
Francesco Bilotta
Rescue & rearchitecting

What is Appropriate Complexity in Software Architecture?

A definition, and a discipline: choosing the level of complexity a problem actually deserves, no more. Why the modular monolith is the sane default.

Francesco Bilotta · · 4 min read

Appropriate complexity is choosing the level of complexity a problem actually deserves, no more and no less. It is a deliberate act of matching: you weigh what the system needs today against what it will cost to maintain tomorrow, and you build to that line. Below the line you are naive and the system breaks under real load. Above the line you are paying interest on structure nobody asked for. The whole discipline lives in finding that line and refusing to drift off it.

I spend most of my working life inside systems that drifted off it. The pattern is always the same: nobody made a single catastrophic decision. Someone reached for the framework that assumed a problem the business did not have. Someone split one function into a service because services felt more serious. Each move was defensible in isolation. Together they built a machine that costs a fortune to change and that the next engineer cannot hold in their head.

Two kinds of complexity, and only one is optional

Fred Brooks drew the line I keep coming back to. In No Silver Bullet he separated essential complexity, the difficulty baked into the problem itself, from accidental complexity, everything we pile on top. Tax brackets are essential. Preventing a double-booking is essential. The problem is genuinely hard and no amount of cleverness makes it simple. The message queue between two functions that could have been one call is accidental. The abstraction built for a second use case that never arrived is accidental. The four layers of controller, service, repository and mapper where the request just reads one row and returns it: accidental, all of it.

Essential complexity you cannot argue with. You model it as faithfully as you can and you carry it. Accidental complexity is the only thing you actually control, and it is where appropriate complexity earns its keep. The discipline is not "make everything simple". Some problems are hard and deserve a serious solution. The discipline is refusing to add difficulty the problem did not hand you.

The economics changed, the judgement did not

There used to be a natural brake on accidental complexity: someone had to type it. Every needless layer, every speculative abstraction, cost human hours, and that cost made people think twice. That brake is gone. Generating code is now nearly free, and when the cost of a bad thing falls to zero you get more of it. I now see systems over-engineered at a speed that was physically impossible three years ago.

This is exactly why appropriate complexity matters more now, not less. The machine will happily produce the elaborate version. It cannot decide where the boundary belongs, and it cannot say no to a feature that doubles the state space for two edge cases. That refusal is judgement, and judgement was always the scarce input. A thing you did not build has no bugs, no upgrade path, no one to page at three in the morning. The best architectural decision is often the one that deletes a requirement rather than serving it.

The modular monolith is the sane default

Given all that, you need a default, a place you start and only leave when the problem forces you out. Mine is the modular monolith: one deployable unit, with firm internal boundaries between modules that could in principle become services but are not yet. It gives you the thing people reach for microservices to get, which is clean separation of concerns, without the tax you pay for it, which is network calls, distributed transactions, and a deployment topology you now have to operate. You keep the option to split later. You just do not pay for it before you need it.

You deviate from this default when the problem proves it needs more, not when an architecture diagram would look more impressive with the split. A module under genuine independent scaling pressure earns its own service. Two edges of the system with truly different availability requirements earn separation. Those are demonstrated needs, not anticipated ones. This is YAGNI applied to architecture: you are not going to need it until you can point at the evidence that you do, and the same rule of three that governs abstractions governs boundaries. Wait for the third real repetition before you extract, because a premature boundary is a loan against a future that may not arrive, and everyone who has to understand the indirection pays the interest.

What I actually sell

Most of what I do on a rescue is subtraction. I remove the queue that connects two things that never needed decoupling, collapse the four layers back to one honest path, and fold the three services that only ever deploy together back into the monolith they were carved out of. The system gets smaller, faster to reason about, and cheaper to change. What I am really selling is not the code I write; it is the judgement about what not to build, and systems that the next person can pick up and survive. Boring is not a failure of ambition. Boring is a feature, and you engineer for it on purpose.