That's Not an Abstraction, That's Just a Layer of Indirection
If you've ever worked on refactoring or improving performance in a software system, you've probably run into a particular frustration: abstraction-heavy codebases. What looks like neatly organized and modularized code often reveals itself as a labyrinth, with layers upon layers of indirection. The performance is sluggish, debugging is a nightmare, and your CPU seems to be spending more time running abstractions than solving the actual problem. This leads us to an important realization: not all abstractions are created equal. In fact, many are not abstractions at all—they're just thin veneers, layers of indirection that add complexity without adding real value.
So what makes a good abstraction?
An abstraction is only as good as its ability to hide the complexity of what lies underneath. Think of a truly great abstraction, like TCP. TCP helps us pretend that we have a reliable communication channel, even though it's built on top of an unreliable protocol, IP. It takes on the complexity of error correction, retransmission, and packet sequencing so that we don't have to. And it does such a good job that, as developers, we very rarely have to peek into its inner workings. When was the last time you had to debug TCP at the level of packets? For most of us, the answer is never.
That’s the sign of a great abstraction. It allows us to operate as if the underlying complexity simply doesn't exist. We take advantage of the benefits, while the abstraction keeps the hard stuff out of sight, out of mind.
The opposite of abstraction
But what about bad abstractions—or perhaps more accurately, what about layers of indirection that masquerade as abstractions? These "abstractions" don’t hide any complexity: they often just add a layer whose meaning is derived entirely from the thing it's supposed to be abstracting. Think of a thin wrapper over a function, one that adds no behavior but adds an extra layer to navigate. You've surely encountered these—classes, methods, or interfaces that merely pass data around, making the system more difficult to trace, debug, and understand. These aren't abstractions; they're just layers of indirection.
The problem with layers of indirection is that they add cognitive overhead. They’re often justified under the guise of flexibility or modularity, but in practice, they rarely end up delivering those benefits. Instead, they make the codebase more complex and more challenging to work with—especially when it comes time to squeeze out more performance or fix a bug.
The real cost of abstractions
We like to pretend abstractions are free. We casually add another interface, another wrapper, and before we know it, we’ve got a whole stack of them. This kind of thinking ignores a fundamental truth: abstractions have costs. They add complexity, and often, they add performance penalties too.
Abstractions are the enemy of performance. The more layers you add, the further you get from the underlying metal. Optimizing code becomes an exercise in peeling back layer after layer until you finally get to the real work. Each layer represents a mental and computational burden. It takes longer to understand what's going on, longer to find the code that matters, and longer for the machine to execute the actual business logic.
Abstractions are also the enemy of simplicity. Each new abstraction is supposed to make things simpler—that’s the promise, right? But the reality is that each layer adds its own rules, its own interfaces, and its own potential for failure. Instead of simplifying, these abstractions pile on the complexity, making systems harder to understand, maintain, and extend.
All abstractions leak
There’s a well-known saying: "All abstractions leak." It’s true. No matter how good the abstraction, eventually, you’ll run into situations where you need to understand the underlying implementation details. This leakage might be subtle—like when you’re trying to understand the performance characteristics (what’s the big O complexity here?)—or it could be more blatant, requiring you to dive deep into debugging to figure out why something isn’t working as expected. A good abstraction minimizes these situations; a bad one turns every small bug into an excavation.
A useful rule of thumb for assessing an abstraction is to ask yourself: How often do I need to peek under the hood? Once per day? Once per month? Once per year? The less often you need to break the illusion, the better the abstraction.
Asymmetry of abstraction costs
There’s also a certain asymmetry to abstraction. The author of an abstraction enjoys its benefits immediately—it makes their code look cleaner, easier to write, more elegant, or perhaps more flexible. But the cost of maintaining that abstraction often falls on others: future developers, maintainers, and performance engineers who have to work with the code. They’re the ones who have to peel back the layers, trace the indirections, and make sense of how things fit together. They’re the ones paying the real cost of unnecessary abstraction.
Conclusion: Use abstractions wisely
This isn’t to say that abstractions are bad—far from it. Good abstractions are powerful. They enable us to build complex systems without getting lost in complexity. But we must recognize that abstractions are not free. They have real costs, both in terms of performance and complexity. And if an "abstraction" isn’t hiding complexity but is simply adding a layer of indirection, then it’s not an abstraction at all.
The next time you reach for an abstraction, ask yourself: Is this truly simplifying the system? Or is it just another layer of indirection? Use abstractions wisely, and remember—if you’re not truly hiding complexity, you’re just adding to it.
© Fernando Hurtado Cardenas.RSS