Back to archive

Engineering

Premature Abstraction Is Worse Than Premature Optimization

Why abstractions created before the pattern is real become slower to remove than inefficient code.

Premature Abstraction Is Worse Than Premature Optimization

Premature optimization has a famous warning label. Premature abstraction deserves a louder one.

I would rather remove an inefficient local implementation than unwind an abstraction that taught the whole system the wrong shape.

The thesis

Premature abstraction is worse than premature optimization because it creates wrong compression that spreads through callers, tests, ownership, and language. Inefficient code is often local. A bad abstraction becomes a contract.

This is not an argument against abstraction. Good abstractions are one of the main ways software stays understandable. The problem is abstraction before the pattern is real enough to name.

The production pattern

A team sees two similar code paths. The duplication feels unpleasant. Someone extracts a shared helper, base class, interface, framework, policy layer, or generic workflow. The code gets shorter. Reviewers like the reduction. The design looks cleaner.

Then the third case arrives and does not quite fit. A flag is added. Then a callback. Then a special validation path. Then a configuration option. The abstraction now contains the differences it was supposed to remove. Callers must learn both the generic concept and the exceptions.

The cost is not only code complexity. The abstraction creates vocabulary. People start discussing the abstraction as if it is a real domain concept. Tests assert its shape. Documentation explains it. New code conforms to it because it exists. Removing it later feels like breaking architecture, even when the architecture was premature.

That is why bad abstractions are sticky. They do not just organize code. They organize thought.

The model

I use a production version of the rule of three, with four checks.

Three real cases: Wait for at least three production-shaped examples before extracting a durable abstraction. The cases do not have to be fully launched, but they should reflect real constraints, not imagined symmetry.

Difference inventory: Before abstracting, list the differences, not only the similarities. Differences in ownership, failure behavior, permissions, latency, data lifecycle, rollout, and user semantics are often more important than duplicated lines.

Compression test: Ask what concept the abstraction names. If the name is technical but the differences are domain-specific, the abstraction may be compressing the wrong thing. "Handler" and "processor" are often signs that the idea is not mature yet.

Escape hatch: Design a way for a future case to leave the abstraction without heroics. If extraction is irreversible, the bar should be higher.

The rule of three is not about numerology. It is about evidence. Two examples often show coincidence. Three examples begin to reveal shape.

Where this goes wrong

The counterpoint is that some abstractions must come early. Public APIs, security boundaries, storage contracts, and platform interfaces may need a stable shape before many consumers exist. Waiting for three full cases can be too late when change cost is enormous.

Another failure mode is worshiping duplication. Duplicated code can hide inconsistent behavior, duplicated bugs, and fragmented ownership. Avoiding premature abstraction does not mean accepting chaos. It means allowing duplication long enough to learn the true pattern.

There is also a team skill issue. Some teams avoid abstraction because they have been hurt by bad ones, then end up with scattered logic that nobody can reason about. The answer is not to avoid abstraction. It is to abstract around stable domain rules and operational contracts, not superficial similarity.

The model also fails if "production-shaped" becomes an excuse to wait forever. At some point, the cost of duplication exceeds the risk of wrong compression. Engineering judgment is deciding when the evidence is enough.

What I do now

When I see a proposed abstraction, I ask what it would make harder to change. Good abstractions make the common path easier while leaving important variation visible. Bad abstractions make the demo cleaner and the fourth requirement worse.

I look closely at the first flags. A flag inside a new abstraction is not always wrong, but it is evidence. It may mean the abstraction is combining cases with different semantics. Two flags during extraction is a stronger warning. A callback that exists only to let callers recover their old behavior is often the abstraction asking to be removed.

I prefer temporary duplication with clear notes over early generalization. "These two flows look similar, but ownership and retry semantics differ. Revisit after the next implementation" is a respectable engineering choice. It preserves learning.

For principal engineers, the concern is organizational coupling. A premature abstraction can force multiple teams to coordinate through a concept none of them actually owns. It can make local changes expensive because everyone must preserve a shared fiction.

When an abstraction is necessary, I want a decision record: the cases it covers, the differences it intentionally exposes, the differences it hides, and the escape hatch. That record helps future engineers know whether to extend the abstraction or let a case diverge.

Abstraction should follow understanding. If understanding is still forming, duplication may be the cheaper teacher.

Closing takeaway

Do not abstract because two things look alike; abstract when real cases prove the same concept, the differences are named, and escape remains possible.