---
title: "Signals That a System Has Grown Too Complex"
description: "The concrete red flags that reveal code or architecture has turned needlessly complex — and where to start simplifying"
url: https://sade.dev/en/journal/signals-of-an-over-complex-system/
lang: en
author: "Muhammet Şafak"
published: 2026-07-25
updated: 2026-09-06
section: Journal
tags: ["architecture","simplicity","decisions","opinion"]
summary: "Complexity does not arrive in one decision; it accumulates in defensible steps, which is why simplification starts from named signals rather than a feeling. Eight concrete signals, then a four-step subtraction: dead code, single-implementation interfaces, premature indirection, then clear boundaries. The target is not the least code but the least unnecessary code — essential complexity cannot be deleted."
---

# Signals That a System Has Grown Too Complex

> Complexity does not arrive in one decision; it accumulates in defensible steps, which is why simplification starts from named signals rather than a feeling. Eight concrete signals, then a four-step subtraction: dead code, single-implementation interfaces, premature indirection, then clear boundaries. The target is not the least code but the least unnecessary code — essential complexity cannot be deleted.

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 does complexity accumulate?

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](/en/journal/the-cost-of-just-in-case-code) 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 do you 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](/en/journal/the-cost-of-just-in-case-code) — 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](/en/journal/why-i-start-with-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.

## Frequently asked

**How do you tell an over-complex system from a genuinely hard domain?**

By which kind of complexity you are looking at. Essential complexity comes from the domain — a tax calculation is complex because tax law is — and can only be modelled honestly. Accidental complexity is what you added: the extra layer, the premature abstraction, the forgotten flag. Only the second kind is worth clearing away.

**Where do you start simplifying?**

In order: delete dead code, collapse interfaces with a single implementation, inline indirection that is called from one place and only forwards an argument, then gather what is left behind clear boundaries. Stop and measure after each step — simplification can also go too far.

**Is an interface with a single implementation always a mistake?**

It is the residue that an abstraction opened just in case leaves behind when the second implementation never arrives. Use the concrete class directly until the abstraction earns a real second use; when a seam is genuinely needed, it comes back in a few hours of refactoring. That is cheaper than carrying it for years.


## Sources

- [No Silver Bullet — Essence and Accidents of Software Engineering (TR86-020): essence, the difficulties inherent in the nature of software, versus accidents, those that attend its production but are not inherent](https://www.cs.unc.edu/techreports/86-020.pdf) — Frederick P. Brooks Jr., University of North Carolina
- [Yagni: the cost of build, delay, carry and repair for a presumptive feature](https://martinfowler.com/bliki/Yagni.html) — Martin Fowler
