Leaky Abstraction
A Leaky Abstraction fails to hide its implementation, so callers depend on details it was meant to conceal, creating hidden coupling and surprises. Design contracts around caller needs with rigorous information hiding, and surface unavoidable details honestly rather than letting them leak.
A Leaky Abstraction is one that does not completely conceal the complexity it claims to hide, so the underlying implementation "leaks" through and callers must understand it anyway. Joel Spolsky's Law of Leaky Abstractions observes that all non-trivial abstractions leak to some degree; the anti-pattern is an abstraction that leaks so badly it provides little protection while creating a false sense of insulation.
Why It Happens
Leaks arise when an interface is designed around its current implementation rather than around the caller's needs. An ORM exposes query semantics that only make sense if you know the SQL it generates. A file abstraction over a network resource works until latency, partial failures, or timeouts surface the network beneath it. Performance characteristics almost always leak: an interface may be uniform, but one implementation is fast and another is catastrophically slow for the same call. Abstractions also leak when they are added late, retrofitted over code that was not designed to be hidden, so the seams show.
Why It Hurts
A leaky abstraction creates hidden coupling: callers come to depend on implementation quirks that are not part of the contract, so changing the implementation breaks them unexpectedly. It produces surprising behavior — operations that look equivalent through the interface behave very differently in reality. It undermines the main benefit of abstraction, which is the freedom to change what is behind it. Worse, it gives a false sense of safety: developers assume they can ignore the internals, then are blindsided when the leak bites in production, often under load or failure conditions the abstraction silently passed through.
Warning Signs
- Using the interface correctly still requires knowing its implementation.
- Callers handle errors or quirks specific to one backing implementation.
- Performance varies wildly across calls that look identical through the interface.
- Documentation explains the internals because the contract alone is insufficient.
- Swapping the implementation breaks callers that should not have cared.
Better Alternatives
Design abstractions around the caller's needs and a clear contract, not around the current implementation. Practice rigorous information hiding, exposing only what callers legitimately require and keeping representation private. Use hexagonal architecture so ports define behavior in domain terms and adapters absorb the messy details. Make failure and performance characteristics part of the explicit contract rather than surprises that leak.
How to Refactor Out of It
Identify where implementation details are bleeding through — look for callers that branch on backend-specific conditions or rely on undocumented behavior. Tighten the contract: decide what the abstraction truly promises and make that explicit, including error semantics and any meaningful performance guarantees. Reshape the interface so it speaks the caller's language rather than echoing the implementation. Where a detail genuinely cannot be hidden (such as the possibility of network failure), surface it honestly in the contract instead of pretending it is absent. Then migrate callers off the leaked behavior. Accept that no abstraction is perfect; aim for one whose leaks are documented and bounded rather than silent and surprising.