There’s a phrase, coined by Joel Spolsky back in 2002, that has quietly organized how I think about tools ever since: all non-trivial abstractions, to some degree, are leaky. It sounds like a complaint. It’s closer to a law of nature, and once you’ve internalized it you make different decisions about what to build on.
Start with what an abstraction is for. It’s a promise that you can use something without understanding what’s underneath. TCP promises reliable, ordered delivery so you don’t have to think about the internet’s actual behavior — packets that vanish, arrive twice, or show up out of order. An ORM promises you can work with objects and never write SQL. A file promises a simple stream of bytes and hides the spinning platter or the flash controller. Each one lets you not-know something, and that not-knowing is the entire productivity dividend of modern software. Nobody could build anything if they had to hold the whole stack, from transistors up, in their head at once.
The catch is in Spolsky’s word: leaky. The abstraction hides the underneath right up until the underneath does something the abstraction didn’t paper over — and then the reality you were promised you could ignore comes flooding up through the seams, usually at the worst possible moment.
The examples are everywhere once you look. The ORM lets you ignore SQL until one innocent-looking loop over a list of objects quietly fires a thousand separate database queries, and suddenly you must understand SQL, indexes, and the query planner to explain why the page takes nine seconds. TCP hides packet loss until you’re on a flaky connection and the “reliable” stream stalls in a way you can only diagnose by understanding the network it swore you’d never have to think about. A remote procedure call is dressed up to look exactly like a local function call — same syntax, same shape — which is precisely the leak, because a local call doesn’t fail because a cable was unplugged. That last one is enshrined in the old “fallacies of distributed computing”: the network is not reliable, latency is not zero, bandwidth is not infinite — every one a place where the “it’s just a function call” abstraction leaks the physics of a real network into your code.
Here’s the part that matters for building, because “everything leaks, despair” is not a useful conclusion. The leaky-abstraction law doesn’t tell you to abandon abstractions — you can’t, they’re the only reason you can build at all. It tells you to choose them with your eyes open, on two axes.
First: how good is the abstraction when it holds, and how bad is the leak when it fails? A great abstraction holds almost always and leaks gently — when it breaks, the reality underneath is learnable and the failure is legible. A bad one holds most of the time and then leaks catastrophically into a layer you have no tools to inspect. Prefer tools whose failures you can actually diagnose over tools that are marginally more convenient on the happy path but opaque when they crack.
Second, and this is the one experience teaches slowly: you don’t get to skip learning the layer beneath the one you work in — you only get to defer it. The abstraction buys you time, not permanent ignorance. The senior engineer isn’t the one who memorized more layers; it’s the one who accepted that the ORM will eventually make them read SQL, that the framework will eventually make them read its source, and who treats the layer below as something they’ll need someday rather than something they’ve been freed from forever. So when you pick what to build on, quietly ask: when this leaks — not if — is the thing underneath something I can come to understand, or a black box I’ll be stuck praying to? That question, more than raw convenience, is what separates a tool you’ll be glad you chose from one you’ll be trapped inside.
Liked this? Get the next one in Working Theory.
Going weekly in August (it's in beta now). One genuinely interesting read on building, the brain, and the science most people missed.