[ Ionut Dumitru ]
EngineeringMar 9, 20267 min read

The abstraction you add before you need it

The abstraction you build for a future that never arrives is just complexity you chose to ship on purpose.

The abstraction that hurts most isn't the one you got wrong. It's the one you built correctly, for a requirement that never came. A clean interface, a tidy set of base classes, a config-driven pipeline — all working exactly as designed, all serving a flexibility nobody ever asked for. You can't even point at a bug. You just notice that every change now costs more than it should, and no one remembers why.

The future you were defending against is the cheapest thing to imagine and the most expensive thing to carry. Imagining it costs an afternoon. Carrying it costs every engineer who touches that code for the next three years, each of them routing around generality that earns nothing.

The trade you actually made

Premature abstraction feels like diligence. You saw a second case coming, so you built the seam now instead of later. But you didn't avoid work — you moved it forward in time and paid interest on it.

Here is the part people skip: a concrete implementation you fully understand is cheaper to generalize later than a wrong abstraction is to unwind. Inlined, duplicated, slightly-repetitive code announces its own shape. You can read three call sites and see exactly what they share. A premature abstraction hides that shape behind an interface that was guessing, and now you have to reverse-engineer the guess before you can change anything.

The duplication you can see is a smaller liability than the coupling you can't.

I once inherited a notification system with a Channel interface, a ChannelFactory, and a registry — built to support email, SMS, push, and webhooks. Two years in, it sent email. Only email. Every email change meant threading a value through three layers of indirection that existed for channels that were never built. Deleting the abstraction and writing a plain function took an afternoon and removed two hundred lines.

Why we do it anyway

Nobody adds speculative abstraction out of laziness. It comes from the opposite impulse — wanting to be the engineer who saw it coming. Generality reads as seniority. A flat function that just does the thing looks junior next to a pluggable architecture, even when the function is the better answer.

There's also a real asymmetry in how the two mistakes get noticed. Ship a rigid design and need to extend it, and everyone watches you scramble — the pain is loud and public. Ship an over-flexible one and the cost is silent: a slow tax on every future change, distributed across people who will never trace it back to the decision. We optimize against the embarrassing failure and ignore the expensive one.

An abstraction should be extracted from code that exists, not designed for code that might.

The honest move is to wait for the second real case before you name the pattern. Not the imagined second case — the real one, in the codebase, with its own constraints. Two concrete uses tell you what actually varies. One use plus your imagination tells you what you fear might vary, which is a different and far less reliable signal.

A rule that holds up

The cheapest version of flexible is code that's easy to delete and easy to change — not code that's easy to extend. Those are not the same property, and we confuse them constantly.

When a real second case shows up, I want the diff to be obvious. That usually means the trigger for abstraction is a duplicated decision, not a duplicated line:

pricing.ts

function priceFor(plan) {
if (plan === "free") return 0;
if (plan === "pro") return 1900;
return 4900;
}

One branch is a branch. The third time, the shape is real.

That function is not elegant, and it does not need to be. It needs to be correct and legible, and it is both. The day a fourth plan arrives with genuinely different logic — usage-based, say, instead of flat — the abstraction extracts itself, because by then I can see all the cases at once instead of guessing at one. The structure follows the evidence rather than running ahead of it.

This is the discipline, and it is harder than it sounds, because it asks you to leave a seam unbuilt while you can clearly picture the day you'll wish you had it. You hold the line anyway. Most of those days never come, and the ones that do arrive cheaper than the standing cost of having prepared for all of them.

What to do at the keyboard

The practical version is small enough to apply in the moment:

  • Write the concrete thing first, even if you can feel the general version forming. Make it work and make it readable before you make it flexible.
  • Count to two. Abstract on the second real, in-tree use case, not the first imagined one. A single example is a data point, not a pattern.
  • Prefer deletable over extensible. If choosing between an abstraction that's easy to remove and one that's easy to grow, take removable — you will be wrong about the direction of growth more often than you expect.
  • When you do add a seam, write down the case that forced it. If you can't name a concrete one, that's your answer.

The strongest systems I've worked in were not the most abstract. They were the ones where the abstractions arrived late, earned their place, and pointed at something real. Everything else was just structure built on a guess — and a guess, shipped, is still a guess. It's only become harder to take back.

So the next time you feel the urge to add the seam before you need it, sit with the question the urge is dodging: what concrete, present thing does this make easier today? If the only honest answer is a future you're imagining, you haven't found an abstraction. You've found a cost, and you're about to choose to ship it.

#Engineering#ArchitectureShare ↗
→ / AUTHOR
Ionut Dumitru
Ionut Dumitru

Full-stack engineer and product designer. Writes about building products where the engineering and the design are the same job.

→ / NEXT
CraftMar 2, 2026
The first draft of a UI should embarrass you
← All writingionutdumitru.com