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.
Simplicity gets used to defend good engineering, but it also gets used to dodge the design work a system actually needs. It can sound like discipline while quietly becoming an excuse to ignore complexity instead of dealing with it.
A simple system is not architecture-free. It is architecture that knows what problem it solves, what it deliberately leaves unsolved, and where unavoidable complexity should live. 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
Most developers learn to make code reusable. Fewer learn when reuse is just extra weight. Premature abstraction sneaks in when someone sees a bit of logic and thinks, “We might reuse this.” Suddenly, there are configs, base classes, factories, or a tiny framework that only makes sense if you can read the developer’s mind about the future.
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.
Abstractions should make code easier to reason about. If they only make the author feel clever, they do not belong in the codebase.
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.
That is where duplication becomes dangerous. The code may still be easy to find, but the meaning has started to drift. A user can get different behavior depending on which path through the system they take.
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. Teams reach for microservices because the last system that used them shipped, not because the current one has shown it needs them. The pattern is not the problem. The projection is.
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.
This is where simplicity gets misused. 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.
At least the complexity is visible and owned. There is one place to reason about the mismatch as it changes, instead of rediscovering it in every caller and integration that accidentally learned part of the truth.
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.
This is where scale discussions often go wrong. 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. More often, it is a success story the business tells itself before reality has a chance to disagree.
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 being DDoSed by 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.
That is the difference. Unnecessary scale work adds complexity beyond the requirements. Ignoring real pressure simplifies the diagram by pushing complexity into runtime behavior. Neither is simple.
Simplicity works only when the design meets the requirements in the simplest, most honest way. Sometimes that means leaving the future alone. Sometimes it means adding the structure that makes the present understandable.
Link to section: Keep the Right Futures PossibleKeep the Right Futures Possible
Architecture mostly decides which costs the system should accept. Complexity does not disappear just because it is kept off the diagram. It shows up later in the code, and people have to understand the system they have to operate or the changes they have to make.
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. Not because those questions are a framework, but because they keep the judgment honest. A boundary that makes a real mismatch explicit is different from a boundary added because boundaries feel clean. A queue that moves real pressure out of the request path is different from a queue added because scale might become a problem someday.
A simple system is not one that refuses complexity. It is one that can explain it. The rule has one home because drift would be dangerous. The mismatch has a boundary because ignoring it would spread the problem. The future stays possible, but it does not get to tax every decision before it arrives.
Simple does not mean basic, naive, or architecture-free. The best architecture is not the one that prepares for every possible future. It is the one that keeps the right futures possible without making the present harder than it needs to be.