DiegoVallejo

The Half-Life of a Good Abstraction

software engineeringarchitectureabstractioncomplexity

The Half-Life of a Good Abstraction

Every abstraction that saves you once will eventually cost you. The trick is knowing when the bill arrives.

I used to think the goal of good software design was to build abstractions so clean they never needed to change. The reality is tamer and more interesting: the best abstractions are the ones you can throw away without guilt. They are temporary scaffolding, not permanent monuments.

The problem is that successful abstractions feel permanent. They start as a relief. A messy domain collapses into a few functions, a class, a small DSL. Suddenly the rest of the codebase is readable. You celebrate. Then the domain shifts, and the abstraction that once clarified starts to obscure. You are no longer simplifying; you are translating requirements through a model that no longer matches the world.

This is the half-life of a good abstraction. It decays because the world it models decays.

The honeymoon

A useful abstraction appears when two conditions meet: repetition and stability.

You notice the same shape appearing in three places. Maybe three different modules all build a query, transform the result, validate it, and map it to a DTO. Extract that into a service. The diff is satisfying. You delete code. The tests pass. Everyone gets a little dopamine hit.

During this phase the abstraction is a compression layer. It removes noise and exposes the signal: what the code is trying to do, independent of how it does it. Good compressions are worth their weight. They make the system legible.

But the honeymoon is fragile. It depends on the assumption that the repeated shape will stay repeated. Real systems violate that assumption in three ways:

  • The exceptions accumulate. The first two callers fit the abstraction perfectly. The third is almost a fit. The fourth is a fit if you squint. The fifth is a fit if you add a flag. The sixth is a fit if you add a flag and a pre-hook and a conditional branch.
  • The underlying platform changes. The database, the framework, the API, the protocol, or the hardware moves. The abstraction was hiding exactly the part that moved.
  • The team forgets the original intent. Comments become stale. The person who wrote it leaves. New teammates use it not because it is the right tool, but because it is there. A local convenience becomes an institutional default.

The abstraction is not wrong. It is old.

The warning signs

There is a predictable set of smells that show up right before an abstraction flips from asset to liability. I keep a loose checklist:

Signal What it usually means
You explain the abstraction before explaining the feature The model has become harder than the problem.
Half the code is escaping the abstraction disableX, rawMode, skipValidation, legacyPath.
Refactoring inside the abstraction is scarier than refactoring around it Too many callers depend on hidden behavior.
You need a meeting to decide what belongs in the abstraction The boundary has dissolved.
Newcomers copy-paste old call sites instead of understanding them The abstraction no longer teaches.

These are not verdicts. They are invitations to measure the half-life. An abstraction with one or two exceptions is fine. An abstraction whose documentation is mostly a list of exceptions is a cage.

Field note: The most expensive abstractions I have seen were not over-engineered from the start. They were under-retired. They survived long enough to become implicit architecture.

When to rebuild, not patch

The hardest decision is not whether an abstraction is leaking. It is whether the leak is a bug or a signal that the abstraction has the wrong shape.

A small leak can be patched. A wrong shape needs to be dissolved.

I use a rough rule: if the next change requires me to add another parameter, another conditional, or another extension point to preserve the abstraction, I stop and ask whether the caller would be clearer without it. Sometimes the answer is yes and the abstraction shrinks. Sometimes the answer is yes and the abstraction disappears entirely.

Here is a concrete example. Imagine a generic createUser abstraction that grew into a createEntity method used for users, organizations, and invitations:

async function createEntity(
  type: EntityType,
  payload: unknown,
  options: CreateOptions
): Promise<EntityResult> {
  const validated = validators[type](payload);
  if (options.skipHooks) return persist(type, validated);
  await runPreHooks(type, validated, options.context);
  const result = await persist(type, validated, options.tx);
  if (!options.silent) await runPostHooks(type, result, options.context);
  return result;
}

It looks reasonable. But the options object is a confession. skipHooks, silent, tx, context: these are not configuration; they are tiny escape hatches for callers whose needs the unified model cannot express. After the third variant, EntityType is no longer a type. It is a dispatch table for three different workflows pretending to be one.

The cleaner move is usually to let each caller own its own flow. There will be duplication at first. But duplication is often cheaper than the wrong abstraction, because duplication is visible. The wrong abstraction hides its cost in indirection.

The discipline

I do not have a universal rule for the perfect level of abstraction. What I try to practice is ruthless locality.

  • Optimize for the reader, not the author. The person debugging at 2 a.m. is the audience.
  • Make boundaries explicit, not clever. If a function does two things depending on a flag, it is two functions.
  • Reserve the right to delete. If an abstraction cannot be removed in an afternoon, it is infrastructure, not a helper. Treat it accordingly.
  • Document the intent, not the mechanism. Explain why the abstraction exists, what problem it solved, and what would make it obsolete. That last part is the expiration date.

This is not a case against abstraction. Abstraction is oxygen in large systems. It is a case against treating abstraction as a permanent achievement. The best abstractions are disposable. They earn their keep, do their job, and retire when the shape of the system changes.

Conclusion

There is no such thing as an abstraction that does not decay. The question is only whether you notice it soon enough to replace it before it calcifies into architecture.

So write the abstraction. Enjoy the cleanup. Then watch it. When the exceptions outnumber the rule, when the callers start escaping, when the abstraction needs a presentation before the feature can be explained, it is time to thank it and take it apart.

The half-life of a good abstraction is not a failure. It is the sign that your understanding of the system has moved forward.

Diego Vallejo, August 2026