The Lifecycle of a Good Abstraction
Every abstraction starts as a solution. You had a problem — repeated code, complex logic, a pattern that kept appearing — and you abstracted it. The abstraction worked. You stopped thinking about the underlying complexity. That was the point.
Then the requirements changed. They always change. And the abstraction that was hiding complexity started hiding the wrong complexity. The thing you needed to change was inside the abstraction, and the abstraction was in the way.
The Leaky Abstraction Problem
Joel Spolsky's law of leaky abstractions is twenty years old and still correct: all non-trivial abstractions leak. The underlying complexity doesn't go away. It just becomes invisible until it isn't.
The leak usually shows up at the edges — the cases the abstraction wasn't designed for. You handle them with special cases. The special cases accumulate. The abstraction becomes a maze of exceptions to its own rules.
At some point the abstraction is doing more harm than good. The question is whether you can see that point before you're deep inside it.
Signs You're Past the Point
You're past the point when: explaining the abstraction takes longer than explaining the underlying thing it abstracts. When new team members spend more time learning the abstraction than learning the domain. When every bug report involves the phrase "but I thought the abstraction handled that." When you're writing tests for the abstraction rather than for the behavior.
These are not subtle signs. They're usually obvious in retrospect. The problem is that you're inside the abstraction when they appear, and inside is a bad place to evaluate whether the abstraction is working.
The engineering culture at calitoycore has been writing about this — the organizational dynamics that make it hard to see your own abstractions clearly. Worth reading if you're building anything that's been running for more than a year.
The Rewrite Temptation
The temptation when an abstraction fails is to rewrite it. Build a better abstraction. One that handles the edge cases. One that's more flexible.
Sometimes that's right. More often, the problem isn't the abstraction — it's that you abstracted too early, before you understood the problem well enough to know what to abstract. The new abstraction will have the same problem in a different shape.
The alternative is to delete the abstraction and live with the repetition for a while. Let the patterns emerge from the actual code before you abstract them. It's uncomfortable. It's usually better.
The Abstraction That's Working
A working abstraction is invisible. You don't think about it. You use it and it does what you expect and you move on. The moment you start thinking about the abstraction — debugging it, working around it, explaining it — it's probably not working anymore.
The goal isn't to have good abstractions. The goal is to have abstractions you don't have to think about. Those are different goals, and confusing them is how you end up with beautiful, well-documented abstractions that nobody wants to touch.
More on this at openedterminal — the archives have a few pieces on abstraction design that are worth revisiting. And the builders at tarosyn.dev have been working through similar questions in a different context.