Over-Engineering vs Under-Engineering: Reading the Context
What This Concept Is
Both of these are failures:
- Over-engineering: building for variation, scale, or flexibility that never arrives. Cost shows up as extra abstractions, more moving parts, slower reviews, and harder debugging.
- Under-engineering: shipping something so rigid that the next obvious change is disproportionately expensive. Cost shows up as rewrites, fire-drill refactors, and bugs in edge cases anyone could have predicted.
The skill is not "pick one and call it 'pragmatic.'" It is reading the context -- the audience, the lifetime, the team, the reversibility -- and shipping the design that matches.
Why It Matters Here
Most design disagreements in the wild are actually disagreements about how much engineering this case deserves. Two competent engineers looking at the same PR can reasonably disagree: one sees a missing Strategy, the other sees premature abstraction. Without a shared way to read context, these disagreements are unsolvable and get emotional.
Concrete Example
Same function, three contexts:
def send_notification(user, message):
smtp.send(user.email, message)
Context A: a one-off internal tool, one engineer, used by three people, lifetime six months. Verdict: appropriate. Adding Strategy + Factory + retry decorators here is over-engineering. When the tool dies, the effort dies with it.
Context B: a customer-facing SaaS product, five engineers, half a million users, SMS and push are on the roadmap within two sprints.
Verdict: under-engineered. Hard-coded SMTP and no retry will become emergencies. Introduce a NotificationChannel interface plus a retry seam now.
Context C: an internal API used by two other services, one-year-old codebase, known plan to swap SMTP for a managed transactional mail vendor next quarter. Verdict: slightly under-engineered. A small Strategy interface is cheap insurance. Decorator for retry can wait until the vendor swap actually happens.
The code is identical. The verdict depends entirely on context.
Common Confusion / Misconception
"Clean code = more abstract." Clean code is the code that matches the problem. Extra abstraction in a throwaway script is worse clean code than concrete code, because it violates "fewest elements."
"Pragmatic = permission to be sloppy." Pragmatism is not a license for global state and no tests. It is a license to not solve problems you do not have. Correctness and tests are rarely on the negotiable side of the tradeoff.
"YAGNI means never generalize." YAGNI means do not speculate. If the generalization is already proven by current requirements (two implementations exist today), adding it is not speculation.
"Refactor later" as escape hatch. In some codebases, "later" never arrives. Use the reversibility lens (next concept): if the decision is cheap to reverse, ship the simpler version; if it is expensive, pay the upfront cost.
"My experience says..." Dangerous on its own, because your last project does not match this one. Anchor to visible facts: current requirements, current team, current codebase.
How To Use It
Before writing or reviewing, read the context on four axes:
- Audience size and skill. Who reads this code? Small team of seniors tolerates subtlety; rotating juniors need explicit names.
- Lifetime and blast radius. Script that dies in six months vs. library with fifty consumers.
- Known variation. How many implementations / configurations exist today? How many are already on the roadmap? (Not "might exist someday.")
- Reversibility. Is this decision a one-way or two-way door? (See next concept.) One-way doors deserve more engineering; two-way doors deserve less.
Then ask two questions before introducing a pattern or abstraction:
- What specific current pressure does this relieve?
- What would I be wasting if I only wrote the concrete version?
If the answer to the first is "none yet" and the second is "almost nothing," skip the pattern.
Check Yourself
- Give a case where Strategy is over-engineering, and a case where not using Strategy is under-engineering.
- Why is "I might need this someday" not enough justification?
- What four axes of context should you read before judging?
Mini Drill or Application
For each change, label it over, under, or appropriate, and justify from context:
- An internal dashboard with one chart type introduces a
ChartRendererinterface with two subclasses, one of which is unused. - A banking service's
Moneyclass usesdoubleinstead ofDecimal. - A side-project CLI tool adds dependency injection via a custom container.
- A public API returns different JSON shapes for the same endpoint depending on an undocumented header.