Abstractions are used to hide broad and complex concepts behind a single idea which is easier to understand. We use them to remove the complicated and unnecessary details and surface an easy to use API.
This is a powerful concept that allows us to reason about complex ideas and communicate more easily. But abstractions have an unfortunate quality - they are sometimes leaky.
The term leaky abstraction was first first coined by Joel Spolsky. It’s used to describe an abstraction that doesn’t manage to fully encapsulate the details that it’s meant to hide.
Hiding the complexity of an operation is not that easy. When one leaks it means that the consumer is being aware of unnecessary details that were supposed to be hidden. Ideally, an abstraction should be a black box. It presents a contract that if you adhere to you get the expected result.
A leaky abstraction forces the user to become aware and get familiar with the underlying implementation. The truth is that every abstraction is leaks details to a certain extent. The more complex the logic we are trying to hide, the more likely it is to leak.
Such abstractions are not only limited to the software world. We use modern technologies and machines without knowing how they work. However, every now and then we have to comply with certain conditions.
Your computer hides its complexity by providing you with a user friendly operating system. But when you want to play a game that your configuration can’t run smoothly you are exposed to the inner workings of the machine. You need to understand what happens inside the black box so you can achieve the desired result.
An example of leaky abstractions in software engineering is the
The browser has the
fetch API that we can use to make AJAX requests. What’s specific about it is that as long as any response is returned,
fetch doesn’t consider it to be an error. It may receive a response with status code 404 or 500 but it will be handled as a normal response object.
axios library encapsulates the
fetch API and provides some useful methods on top of it. However, requests that don’t return successful status codes are treated as errors. This is a difference from the normal
fetch API that the user of the library needs to be aware of. To successfully use this abstraction, one needs to understand its inner workings.
ORMs intend to hide the complexity of writing SQL by providing us with objects whose methods represent the different database operations. Those data structures are easier to manage and their API is more readable. You call the appripriate methods and you needn’t worry about the SQL that is generated under the abstraction.
But whenever you need to run a truly complex query an ORM may not give you the performance you desire. This forces you to think about the implementation of the tool and find an alternative method to run the database operation.
I use the last part in each of my articles to focus on solutions. A remedy to the problem that we are discussing. Whether it’s over engineering, communication, wrongly built abstractions or anything else. We can try a different approach to a problem, aiming for a better outcome.
Abstraction is a fundamental concept to programming and it’s one of the first things taught in every OOP course. But despite that it’s not easy to get them right. After years in the industry I’m still unsure whether my decisions are correct or not.
As we mentioned, every abstraction that aims to hide a complex enough idea is leaky to a certain extent.
So what can we do? Well, we know that quality is tightly related to the amount of complexity that an abstraction hides. We can’t completely stop the leakage but we can reduce it by reducing the complexity. If we can make it simpler there is a smaller chance of it exposing details.
However, complexity can be reduced only if it was our fault in the first place. If a different architecture can make the abstraction easier to reason about then we should consider refactoring it. But if it’s an intrinsic value of the business logic being hidden, there’s nothing we can do.
Creating another abstraction over the leaky one is an option. We can try to address the issues and provide an API that doesn’t require internal knowledge of how the abstraction works. This is a decision that needs to be made with care, though.
By adding another layer we hide the leak but we increase the overall complexity of the software. If we go that route we need to be sure that we can address all the leaks in that extra abstraction. Otherwise we’d end up with higher complexity and even harder leaks to manage.
It’s important to say that leaky abstractions are not a code smell or an anti pattern that needs to be addressed and removed from the codebase. They are unavoidable in every complex software and as a whole. But maybe with time, as we build on top of the abstractions of past technologies we’ll get better at minimising the leaks.
- A leaky abstraction is one that doesn’t manage to fully encapsulate the details that it’s meant to hide.
- Complex ideas can rarely be fully encapsulated.
- Your computer is an abstraction but if you want to play a game that it can’t support you are immediately exposed to its limitations.
- Leaky abstractions are not an anti pattern or a code smell.
- Any complex software will have to deal with them.