Articles
8 min read
Software DesignArchitectureCraftComplexity

Abstractions That Earn Their Place

Every abstraction is a loan against future clarity. Good engineering is knowing which ones earn their interest — and deleting the ones that don't.

Abstractions That Earn Their Place

There is a kind of codebase that looks clean from a distance and feels exhausting up close.

Everything has a layer. Every concept has a base class, interface, provider, adapter, manager, registry, factory, or strategy. The architecture diagram looks tidy. The names sound professional. The intent was good.

But making a small change means walking through five files to understand one behaviour.

The abstraction is not helping anymore. It is the thing in the way.

I think this is one of the harder judgement calls in software design. Most engineers learn early that duplication is bad, coupling is bad, and abstractions help us manage complexity. That is all true enough to be useful.

It is also incomplete.

Abstractions are not free.

Every abstraction is a loan against future clarity. Sometimes it pays for itself many times over. Sometimes it charges interest forever. Good engineering is knowing which ones earn their place — and being willing to delete the ones that do not.

Abstractions have a cost

An abstraction is not just a cleaner name for a piece of code.

It is a claim about how the system should be understood.

That claim carries cost. It adds indirection. It creates a new concept for people to learn. It couples the code to the abstraction's worldview. It decides which differences matter and which ones should be hidden.

When the abstraction is right, that trade is worth it. The system becomes easier to reason about. Repeated complexity moves behind a boundary. Change becomes safer. The name gives the team a shared language.

When the abstraction is wrong, the opposite happens.

The code has to keep explaining itself through something that no longer fits. Every new requirement becomes an exception. Every call site knows a little too much. The abstraction leaks, but the team keeps pretending it does not. Eventually the abstraction becomes load-bearing, and nobody wants to touch it because too much depends on the shape.

That is the danger.

The cost is often invisible when the abstraction is introduced. It feels tidy. It removes duplication. It creates a pattern. The pain arrives later, when the real system starts disagreeing with the model.

"Earns its place" is the test

I like the phrase "earns its place" because it forces a useful question.

Does this abstraction remove more complexity than it introduces?

Not in theory. In this codebase. For this team. At this point in time.

A good abstraction should be justified by real use. Real call sites. Real repetition. Real concepts in the domain. Real variation that needs to be controlled.

Imagined future use is much weaker.

I have seen a lot of abstractions created for the third or fourth use case before the second one exists. The code gets structured around flexibility the system may never need. Then the real requirements arrive and the abstraction is somehow both too general and not quite right.

That is a bad trade.

I would rather start concrete and wait for the pattern to show itself.

A little duplication is often cheaper than the wrong abstraction. Duplication can be removed once the shape is obvious. A premature abstraction can spread through the system and become expensive to unwind.

The usual rule of thumb is to wait until you have seen the thing three times before naming it. I do not treat that as a law, but I like the instinct behind it.

Let the code teach you what the abstraction should be.

Duplication is not always the enemy

Engineers can be too quick to eliminate duplication.

Sometimes duplication is a smell. Sometimes it is just two similar things that are not yet the same thing.

That distinction matters.

If two pieces of code look similar today but are likely to change for different reasons, forcing them behind one abstraction can create worse coupling than leaving them separate. The duplication is visible, but the coupling becomes hidden.

Hidden coupling is more dangerous.

The team thinks there is one concept. Product changes one path. Another path breaks because the abstraction was sharing behaviour that only looked common. Now everyone is afraid to change it.

This is where senior judgement matters.

The question is not "does this code repeat?"

The question is "is this repetition expressing the same concept?"

If it is the same concept, abstraction may help. If it is only similar structure, be careful.

Code can look alike and mean different things.

Leaky abstractions need honesty

All abstractions leak eventually. The question is whether the leak is acceptable.

Sometimes the right move is to improve the abstraction. Sometimes the right move is to split it. Sometimes the right move is to delete it. And sometimes the right move is to stop pretending the underlying thing can be hidden.

This comes up a lot around infrastructure, databases, APIs, queues, frameworks, and third-party services.

A team creates a clean wrapper to hide a dependency. At first, that feels sensible. Then the dependency's real behaviour starts to matter: transaction semantics, retry rules, consistency guarantees, rate limits, pagination, latency, permission models, failure modes.

The wrapper grows more and more escape hatches until everyone is effectively programming against the underlying system anyway, just through a worse interface.

At that point, honesty is better.

Let the important details show. Name the dependency. Teach the team its constraints. Keep the boundary where it actually helps, not where you wish the complexity disappeared.

A good abstraction simplifies without lying.

Boundaries are the abstractions that matter most

The most expensive abstractions are not usually small helper functions.

They are boundaries.

Module boundaries. Service boundaries. Data boundaries. Ownership boundaries. Domain boundaries.

These are the abstractions that compound hardest because they shape how teams think, build, deploy, and change the system.

A good boundary can protect a team for years. It lets one part of the system evolve without destabilising everything else. It makes ownership clearer. It reduces coordination. It gives the architecture a shape people can reason about.

A bad boundary does the opposite.

It creates cross-service chatter. It splits data that should be owned together. It hides coupling behind APIs. It turns local changes into distributed ones. It makes teams negotiate constantly because the architecture drew the line in the wrong place.

That is why I am much more cautious with architectural abstractions than local ones.

A bad helper can be removed. A bad service boundary becomes a political and operational problem.

The bigger the abstraction, the more evidence I want before it exists.

Naming is part of the abstraction

Naming is not cosmetic.

An abstraction is a claim about meaning, and the name is how that claim enters the team's language.

A bad name is a bad claim.

Names like Manager, Processor, Handler, and Service are not always wrong, but they are often a sign that the abstraction has not earned a clearer concept yet. The code is doing something, but the team has not named what it means.

Good names compress understanding. They tell the next engineer what distinction matters. They make the domain clearer. They reduce the need to inspect implementation details every time.

Bad names create fog.

Worse, they spread. Once a vague abstraction exists, other code starts organising around it. The team inherits the vagueness as architecture.

That is why naming is design work.

If we cannot name the abstraction clearly, there is a decent chance we do not understand it well enough yet.

The delete test

One of my favourite tests for an abstraction is simple:

What happens if we remove it?

If deleting the abstraction and inlining the behaviour makes the code clearer, the abstraction probably is not earning its place.

That does not mean every abstraction should be deleted the moment it adds a little indirection. Indirection can be the right price for isolation, reuse, testability, or a stable boundary.

But the delete test is useful because it breaks the spell.

We often keep abstractions because they feel like architecture. They look intentional. They have names. They were probably introduced by someone thoughtful. Removing them can feel like going backwards.

Sometimes it is the opposite.

Sometimes deleting an abstraction is how the system gets simpler.

An abstraction you are afraid to touch has stopped being an asset. It might still be necessary, but it deserves scrutiny. Is it protecting complexity, or preserving it? Is it clarifying the system, or forcing the team to carry a model that no longer fits?

Good codebases are not just built by adding the right structure.

They are maintained by removing structure that stopped paying rent.

Abstract on evidence, not anticipation

The best abstractions I have seen usually arrive after a period of concrete work.

The team has built the thing once. Then again. Then a third variation appears. The differences are visible. The stable part is obvious. The name starts to emerge naturally because people are already using the concept in conversation.

At that point, abstraction is not a guess. It is a compression of something the system already knows.

That is very different from anticipating a future pattern and designing for it too early.

I do not think the answer is to avoid abstraction. That would be its own kind of immaturity. Good systems need structure. Good boundaries matter. Shared language matters. We cannot build everything as one flat pile of concrete code.

The point is to make abstractions earn their place.

Default to concrete. Let the real use cases appear. Name the concept when the evidence is there. Keep the abstraction honest. Watch for leaks. Delete the ones that no longer remove more complexity than they introduce.

That is the craft.

Every abstraction is a loan against future clarity.

Some earn their interest.

The rest should be paid off.

2026 Ben Sutherland