All articles
Engineering 8 min readMarch 12, 2024

Over-Engineering Is the Most Socially Accepted Form of Technical Debt

Under-engineering gets flagged in code review. Over-engineering gets praised. This asymmetry is why early-stage products accumulate layers of abstraction they'll never need.

Every experienced engineer has seen both failure modes: the startup codebase where nothing is abstracted and the same logic is copy-pasted seventeen times, and the startup codebase where everything is abstracted into a framework of frameworks that requires understanding six layers of indirection to change a button label. The first failure mode is obvious and gets fixed. The second failure mode gets architecture diagrams and conference talks.

Over-engineering is technically debt — it increases the cognitive cost of making changes, slows onboarding, and creates accidental dependencies between unrelated parts of the system. But because it signals technical sophistication rather than cutting corners, it rarely gets treated as the problem it is.

The Most Common Forms

Premature generalization. Building a plugin system before you have two plugins. Creating an abstract factory for an object that has one concrete implementation. Designing a configuration system that supports every deployment model when you only have one. Generalization is valuable when you have two or more concrete cases to generalize. Before that, it's complexity masquerading as flexibility.

Framework-first thinking. Reaching for a full framework when a simple function would do. Introducing an event bus to solve a problem that three direct function calls would handle. Adding a state management library to manage three pieces of state. Frameworks are valuable at scale. Before scale, they're cognitive overhead that future engineers have to navigate.

Optimization before measurement. Caching values that are read once per session. Denormalizing data structures that are never queried at significant volume. Adding database indices to queries that run twice per day. Optimizations that solve imaginary performance problems add real complexity without adding real value. Measure before optimizing.

Why Code Review Doesn't Catch It

Over-engineering survives code review for a structural reason: it's evaluated against the wrong standard. Reviewers ask "is this code correct and safe?" not "is this code as simple as it could be?" The correctness standard is met — over-engineered code often works perfectly. The simplicity standard isn't applied because there's no culture of explicitly asking whether the complexity is justified.

The question that catches over-engineering is: "What's the simplest thing that would work here?" When a reviewer asks this question and the answer is significantly simpler than what was proposed, a useful conversation ensues. When this question is never asked, complexity accumulates unremarked.

The Right Level of Abstraction

There's a principle from Kent Beck: make it work, make it right, make it fast — in that order, and only when necessary. A corollary applies to abstraction: write the concrete case first. Write a second concrete case. If the pattern is clear and the abstraction would make both cases simpler, abstract. If not, leave them concrete and wait for the third case.

This isn't a rule against abstraction — it's a rule against speculative abstraction. The abstractions that aged best in the codebases I've worked in were the ones that emerged from real duplication and solved a demonstrated need. The ones that aged worst were the ones that anticipated a need that never materialized.

Simplicity is a feature. It's harder to build than complexity and harder to defend in a code review. Build the culture that asks "is this as simple as it needs to be?" before asking "is this sophisticated enough to be proud of?"

Try CodeMouse on your next PR

Free AI code review on every pull request. Bring your own API key — no subscription needed.

Install on GitHub — Free