At the end of her first week, an engineer who had just joined the team asked: “Why does adding a single field take two days?” The question was innocent; the diagnosis was clear. Nobody had sat down one day and decided to make that system complex. The complexity had simply accumulated.

That is the heart of it: complexity doesn’t arrive in a single decision, it builds up unnoticed. To undo it, you first have to be able to see it.

How complexity accumulates

No complex system decides to be complex. Each step looks reasonable on its own: “let’s open an interface for now”, “let’s put a flag on this case”, “one more layer in between”. Each is small, each is defensible.

Their sum is not defensible. Complexity is not an event but an accumulation — and an accumulation stays invisible until it is named. That is why simplification cannot start from a feeling; it starts from concrete signals.

The concrete signals

The red flags, not open to debate, that tell you a system is more complex than it needs to be:

  • You open a lot of files to understand a single feature. If seeing where a request goes takes six files and four levels of indirection, the problem isn’t you, it’s the structure.
  • A one-line change touches a dozen files. To add one field: migration, model, DTO, mapper, interface, factory, test fixture… The cost of a change is not proportional to the size of the change.
  • Interfaces with a single implementation. Abstractions opened “just in case” whose second implementation never arrived. This is the residue speculative generality leaves in a codebase.
  • Onboarding takes weeks. In a mature, lean system a new engineer is productive in the first days. If it takes weeks, the code isn’t explaining itself.
  • Nobody can draw the system on a whiteboard. An architecture that can’t be described as “this box connects to that one” is an architecture that isn’t understood.
  • The test setup is longer than the test itself. The ten mocks you need to stand up to test one behavior are a confession that the behavior depends on far too much.
  • “Don’t touch that, it’ll break.” If a region of code has been declared untouchable, that region is already broken — it just hasn’t blown up yet.
  • Config, flags, and options nobody uses. Every option that was opened and forgotten is a branch that everyone reading the code has to account for.

If two or three of these signals are present in a system, the debate isn’t “is it complex” but “where do we start”.

Where to start simplifying

Simplification is not a rewrite; most of the time it’s a subtraction. In order:

  1. Delete dead code. Unused flags, methods never called, branches never reached. The lowest-risk, highest-return step — deleted code has no bugs.
  2. Collapse single-implementation interfaces. Use the concrete class directly until the abstraction earns a real second use. When a seam is genuinely needed — once the need is proven — it comes back in a few hours of refactoring.
  3. Inline premature indirection. Remove the layers that are called from a single place and only pass an argument on to another function.
  4. Clarify boundaries. Gather the remaining complexity behind clear boundaries, like in a modular monolith — so the next accumulation shows up early.

After each step, stop and measure. Simplification can also go too far; the goal is not “the least code” but “the least unnecessary code”.

Complexity isn’t always bad

An important distinction: there are two kinds of complexity. In Fred Brooks’s terms — essential and accidental.

Essential complexity comes from the domain itself. A tax calculation is complex because tax law is complex; you can’t delete that, you can only model it honestly. Accidental complexity is what you add: the unnecessary layer, the premature abstraction, the forgotten flag.

The aim of simplification is to clear away the accidental — not the essential. Making a system “too simple” and ignoring the domain’s real complexity is also a mistake; that complexity doesn’t get deleted, it just moves to the wrong place — usually into the calling code.

Good architecture is exactly as complex as the domain is; no more.


Complexity accumulates unnoticed; that is why knowing by heart the signals that make it noticeable is an engineering skill. If you can’t name the signal, you can’t start simplifying either.

Keeping a system simple takes more effort than building it simple — and that effort is worth it.