Skip to main content
Jimmy Jansen

Simplicity Is an Architectural Decision

Simplicity is not the absence of design. It is the result of knowing which complexity has earned its place, where it belongs, and which imaginary futures are not worth paying for.

I want a system to be easy to understand and change. Sometimes that means leaving an abstraction out. Sometimes it means adding a boundary so the rest of the system has less to understand.

Zero complexity is not the goal. The value is usually in the hard parts. Skip that work, and the system does not stay simple. It just hides the mess until the mess becomes architecture.

Link to section: Abstraction Is Either a Gift or a TaxAbstraction Is Either a Gift or a Tax

Reuse can save work, but it can also add structure before that structure is useful. A small piece of logic can end up surrounded by configs, base classes, factories, or a tiny framework for requirements that are still guesses.

None of those things is bad on its own. I like abstraction. Good abstractions are why large systems can be understood at all. The problem starts when the extra structure becomes something every reader has to understand before it helps them understand anything else.

I spend a lot of time understanding codebases I did not write. In that situation, abstraction is either a gift or a tax. A good abstraction helps me build a smaller mental model of the system. It tells me which details I can safely ignore until I need them. A bad abstraction puts itself between me and the behavior I am trying to understand.

This is where the cost shows up. A feature should be easy to follow through the codebase. When abstraction gets in the way, you stop tracing behavior and start decoding structure. The code may look clean, but now every reader has to understand a future the system may never need.

Mental overhead adds up fast. Onboarding slows. Debugging slows. Every change feels riskier because nobody knows if they are changing real behavior or just disturbing the machinery around it.

An abstraction earns its place when it makes behavior easier for the next engineer to follow. If readers must decode the framework before they can trace a change, the structure is adding cost.

Link to section: Duplication Is Cheap Until It Starts LyingDuplication Is Cheap Until It Starts Lying

Most rules about duplication are too blunt to be useful on their own. Repeated code is not automatically debt. A small piece of duplicated logic is easy to find, change, and delete. If two parts of the codebase filter an array the same way, nobody should lose sleep over it. The problem starts when repetition stops being just code.

The danger is duplicated meaning. If the same permission rule lives in three places, the problem is not that the code was copied. The problem is that one version eventually changes, and the others do not. Now the system has different answers for which rule is correct.

The real question is not “Does this appear twice?” It is “If this changes, what happens if one copy is missed?”

When the answer is “not much,” duplication is probably fine. When the answer is “the system starts lying about its own rules,” that rule needs a single source of truth. That is where repetition turns into risk.

Link to section: Speculative Architecture Is ExpensiveSpeculative Architecture Is Expensive

Architecture always involves some guesswork. You make bets about where the product is going, how the business might grow, where the technical pressure will show up, and which decisions will be hard to change later.

The mistake is treating every possible future as if it is already real.

A system might need to scale, migrate, or support cleaner boundaries later. Those are valid concerns, but they are not automatic requirements. When the future is still a guess, every piece of architecture built for it has to justify the cost it adds today.

That cost is easy to underestimate because speculative architecture often sounds responsible. Nobody wants to be the person who ignored scale, blocked a migration, or created a system that could not change. But preparing for every possible failure mode is not free. Someone has to build it, test it, maintain it, and explain it.

Past pain makes this harder. The painful incidents stick. After a bad migration, it is tempting to make every system migration-ready from day one. After a scaling failure, every traffic spike starts to look like the system might fall over. Those instincts are understandable, and sometimes they are exactly what the system needs. They become expensive when lessons from the past are turned into requirements without checking whether the current system has the same constraints.

Success has the same effect from the other direction. A solution that worked once starts to feel proven, then starts to feel default. Before carrying it into another system, I want to check which constraints made it useful last time and whether they apply here.

Good architecture keeps the future in mind, but it does not let every imagined failure mode shape the present. Possible is not the same as necessary, and necessary later is not the same as worth paying for now.

Link to section: Complexity Should Live Where It Can Be OwnedComplexity Should Live Where It Can Be Owned

Speculative architecture is expensive, but real complexity still needs somewhere to go.

If the complexity exists only because the future might demand it, you are paying for a problem the system does not have yet. But when the complexity is already there, avoiding structure does not make the system simpler. It just pushes the problem into places where nobody owns it.

A boundary or translation layer can seem unnecessary when you only look at the diagram. Sometimes that is true. Extra structure can become noise like any other abstraction. But if the rest of the system would otherwise need to understand the same awkward rules in slightly different ways, the structure may be carrying its weight.

I have seen this most clearly when systems use the same words but mean different things. If both systems have a concept called “asset,” it is tempting to pretend they are talking about the same thing. The word is the same. The assumptions are not.

Ignoring that mismatch does not remove the complexity. It spreads it. Callers start making local decisions about what the concept means. Integrations carry assumptions that are not quite written down. Every change gets harder because the real decision is nowhere and everywhere at the same time.

This is where a translation boundary can be the simpler choice. It does not make the complexity disappear. It gives the mismatch a clear place to live, with the ownership, tests, monitoring, and operational care it needs. A system like this often sits on the critical path. When it goes down, everything depending on it can go down with it.

Sometimes the right move is to isolate complexity in one place. Not because centralization is always good. It is not. Because existing complexity will either be owned somewhere or leak everywhere.

Link to section: Design for Real Pressure, Not Imaginary ProblemsDesign for Real Pressure, Not Imaginary Problems

A design is not simple because it has fewer pieces. It is simple when it satisfies real requirements with the least complexity that can honestly support them.

Building for a launch that might explode, a customer base that might arrive all at once, or a product that might become much larger than it is today can sound responsible. Sometimes it is.

If the requirements do not call for that version of the system, the extra architecture does not simplify anything. It makes the present pay for a future that may never arrive.

The opposite mistake is just as expensive. A design can look simple on paper yet fail to meet the requirements in practice. That usually happens when the design ignores the work the system actually has to do.

I have seen this happen in a service where a single request could lead to a large amount of downstream work. The smallest design on paper was to keep the flow direct and synchronous. No queue. No caching. No bulk operations. Just let the service handle the work.

That looked simple until the system was tested under realistic conditions. The service was effectively DDoSing itself. That was the point where the design had to change.

At that point, the queue, cache, and bulk endpoints were not extra architecture for some theoretical future. They were the simplest design that actually matched the problem. The queue made the work explicit rather than hiding it in the request path. The cache made repeated work visible and avoidable. The bulk endpoint gave the caller a way to express the actual operation rather than forcing the system to simulate it by storming it with a series of smaller calls.

On paper, that is more structured. In practice, it is simpler because the responsibilities are clear. You can see how the work flows through the system, which part owns which problem, and where the pressure should go.

Link to section: Keep the Right Futures PossibleKeep the Right Futures Possible

Someone has to understand, change, and operate whatever we build. That work is part of the cost of an architectural decision.

Before adding complexity, I want to understand what it protects, what makes it worth paying for now, and where it will live once it exists.