Notes on Systems

Abstractions That Age Poorly

April 2, 2026

Good abstractions compress complexity. Bad abstractions relocate it.

The best ones disappear. You stop noticing them. The worst ones announce themselves on every change, every new requirement exposing another edge case the abstraction was not designed for. The team spends more time working around the abstraction than working through it.

Most abstractions that age poorly were not bad ideas. They were good ideas applied too early, before the problem was understood well enough to know what should be hidden and what should remain visible.

Early abstraction is seductive

You see a pattern forming — two or three things that look similar — and the instinct is to unify them. Extract a base class. Create a shared interface. Build a generic handler. The code becomes shorter. The duplication disappears. It feels like progress.

The cost arrives later. The things that looked similar turn out to be similar only at the surface. As requirements diverge, the shared abstraction accumulates conditionals. It grows parameters. It develops modes. The generic handler now has seven configuration options, four of which exist to accommodate a single consumer. The abstraction that reduced complexity has become the primary source of it.

The cleverness tax

The initial abstraction was elegant. Maintaining it is not. Every modification requires understanding the full surface area of the abstraction, not just the part you are changing. New team members cannot reason about one use case without understanding all of them. The cost is no longer paid once. It is paid on every change.

Local elegance creates this problem more often than carelessness does. A developer writes a beautiful generic solution. It works. It is well-tested. It handles the current use cases with minimal duplication. Within its own scope, it is correct. But zoom out, and the system now has a coupling point that resists change. Two features that should evolve independently are bound together by a shared interface that neither fully fits.

The failure is not in the code. It is in the assumption that the things being unified will continue to evolve together. That assumption is rarely tested at the time the abstraction is introduced. It is tested later, when one use case needs to move in a direction the abstraction does not support.

When the generic bus breaks

I have watched this happen with event systems. A team builds a generic event bus early in a project's life. Every domain event flows through the same pipeline. It works well for the first year. Then one domain needs strict ordering because payment reconciliation depends on event sequence. Another needs transactional boundaries. A third needs fire-and-forget because latency matters more than delivery guarantees. The generic bus, originally a clean solution, becomes a negotiation layer where every team must accommodate every other team's requirements.

The event bus was not a mistake. It was a reasonable choice given what was known at the time.

The mistake was not revisiting that choice when the domains diverged. The abstraction became structurally necessary before anyone noticed, and by the time the cost was visible, replacing it required coordinated effort across multiple teams.

The pattern

Abstractions that age poorly tend to share a few traits. They unify things that are similar now but will diverge later. They hide differences that turn out to matter. They are introduced before the problem space is stable. And they become harder to remove the longer they exist, because more code depends on them.

The alternative is not to avoid abstraction. It is to delay it. Write the duplication. Let the pattern prove itself over three or four instances, not two. Wait until you understand not just what is similar, but why it is similar. The shape of a good abstraction comes from understanding the forces that will act on it, not from the code that currently exists.

Duplication is cheaper than the wrong abstraction

Duplicated code can be changed independently. A bad abstraction forces coordinated change. Duplicated code is obvious. A bad abstraction hides its cost behind a clean interface that no longer reflects reality.

The hardest part is resisting the urge to clean things up too early. Duplication feels like a problem. It looks unfinished. It triggers the same instinct that makes refactoring attractive — the desire for order. But premature order can be more expensive than temporary disorder.

The abstractions that last are the ones introduced after the problem was understood, not before. They are smaller than you would expect. They hide less than you would think necessary. They leave room for the cases that have not appeared yet, not by being generic, but by being narrow enough that they do not get in the way.

Simplicity is not the absence of abstraction. It is the presence of the right ones, introduced at the right time, hiding the right things.

March 27, 2026

Refactoring as a Signal

The code that needs refactoring most often is rarely the worst code. It is the code sitting on top of a structural assumption that no longer holds.

March 18, 2026

Defaults Nobody Revisits

Best practices are useful until they become borrowed conclusions that no longer match the system’s actual constraints.

March 12, 2026

The Carrying Cost of Flexibility

Speculative flexibility often looks responsible at first, then quietly raises the cost of understanding, testing, and changing the system.

← Back to all notes