A meditation on complexity, simplicity, and why the most impactful engineering often involves removing things rather than adding them.
Of all the lessons engineering has taught me, the most durable one is this: complexity is the enemy. Not a tradeoff to be balanced, not a cost to be managed, but the actual adversary. Almost every system I have seen die was killed, slowly, by complexity it could not afford.
This essay is my attempt to argue for simplicity as a discipline, not a slogan.
Why complexity is the default
If you do nothing, complexity grows. Every feature you ship, every dependency you add, every special case you handle, every meeting where you avoid saying no, all of it accumulates. The system gets harder to understand and harder to change. Velocity drops. Bugs increase. Onboarding takes longer. New ideas get harder to evaluate, because their cost has to include the cost of integrating them with everything that already exists.
Simplicity, by contrast, has to be defended. Nobody comes into a planning meeting saying "let us remove a feature." Removal is invisible work. Addition is the kind that gets you promoted, at least at most companies. So the gradient of every system points toward more complexity, and resisting that gradient is most of what I think senior engineering actually is.
The two kinds of complexity
I find it useful to separate two things that often get conflated.
Essential complexity comes from the problem itself. If you are building a tax calculation system, the complexity of the tax code is essential. You can wrap it, hide it, document it, but you cannot remove it. Essential complexity is the price of admission.
Accidental complexity comes from how we chose to solve the problem. The framework choices, the abstraction layers, the configuration sprawl, the deployment pipeline that has six steps when it could have two. Accidental complexity is the part we made up.
Almost all of the complexity I have spent my career fighting is the second kind. From teams I have worked with, the most common failure mode is treating accidental complexity as if it were essential, and giving up on simplifying it.
Where it sneaks in
A few patterns I see again and again.
Premature abstraction
You see a pattern repeated three times. You factor it out into a generic helper. Six months later the call sites have all evolved in slightly different directions, but the helper is now a contract that none of them quite fits, so they all have flags and exceptions baked into them.
The cost of waiting one more iteration to abstract is almost always lower than the cost of abstracting wrong. I now wait until I have at least four or five examples and a clear sense of what they actually share.
Tooling as identity
Some teams collect tools the way other people collect books. A new linter every quarter. A new dashboard every month. A new framework every year. None of these is bad in isolation. Together they form a maintenance burden that nobody owns.
Earlier in my career working on regulated systems, I learned that boring infrastructure is a feature, not a failure. The systems that survived twenty years were the ones built on five well-understood pieces, not fifty fashionable ones.
Configuration sprawl
Every team I have ever joined has at least one feature flag that nobody remembers, three environment variables that nobody knows the source of truth for, and one config file with comments that read like archeology. Configuration is complexity in disguise. It looks like flexibility. It is mostly future confusion.
Defensive complexity
This one is hardest to push back on. Someone adds a try-catch, a retry layer, a circuit breaker, just in case. Each one looks responsible. Together, they obscure the actual failure modes of the system, because every layer slightly transforms the error before it reaches you.
What simplicity looks like in practice
Simple does not mean naive. The simplest version of a system is the one that solves the problem with the fewest moving parts that you fully understand.
A few practical heuristics:
- Boring stack first. Start with Postgres, Next.js, and a single deployment target. Add specialization only when you can name the specific reason.
- One way to do each thing. If your codebase has three caching layers, pick one and migrate. Three options is rarely worth the optionality.
- Delete more than you add in any refactor. If a refactor adds net lines of code, ask whether you have actually simplified anything.
- Document the bad path. Most complexity is justified with hypothetical edge cases. Write down the actual failures you have seen. Most of the hypotheticals will quietly disappear.
- Run a quarterly complexity audit. Look for unused features, dead config, dependencies you do not need. The hour you spend deleting them buys you a month of clarity.
Why this matters more now
Two trends make simplicity even more valuable than it used to be.
The first is that AI tooling makes producing code cheaper than ever. When code is cheap, the constraint moves to what your team can hold in their heads. Simple systems scale to that constraint. Complex ones do not.
The second is that the half life of frameworks keeps shortening. The best defense against churn is to have less code that depends on any particular tool. Simplicity is portability.
If your system is starting to feel hard to reason about, that is the warning. Do not wait for it to become a fire. The cheapest moment to remove complexity is always the moment you first notice it.
If you want a second pair of eyes on a system that has gotten away from you, an architecture review is the thing I do most often. The work is mostly removal.
References
Tagged
Sri Vardhan
Independent technology studio of one. I help founders and small teams ship serious software without the consultancy overhead. More about me.