Abstraction
“All non-trivial abstractions, to some degree, are leaky.” — Joel Spolsky
Understand This First
- Shape – recognizing the shape of a system helps you choose the right abstractions.
Context
Software systems are too complex to hold in your head all at once. Abstraction is the tool that lets you ignore what doesn’t matter right now so you can focus on what does. It operates at the architectural scale, though every level of software construction depends on it. When you call a function without reading its source, use a library without studying its internals, or prompt an AI agent without knowing how it tokenizes your words, you’re relying on abstraction.
Problem
How do you manage complexity that exceeds what a single person (or a single agent context window) can hold at once?
Forces
- Real systems contain more detail than anyone can reason about simultaneously.
- Hiding detail makes things simpler, but hiding the wrong detail causes surprises.
- Too many layers of abstraction make it hard to understand what is actually happening.
- Too few layers force you to think about everything at once.
Solution
Create boundaries that separate what something does from how it does it. An interface is the visible face of an abstraction: it tells you what you can do. The implementation behind it is the hidden body: it handles how. A good abstraction has a stable, understandable interface that you rarely need to look behind.
The art is in choosing what to hide. A database abstraction that hides the query language is useful; one that hides whether your data is persisted is dangerous. The right level of abstraction depends on who the consumer is and what decisions they need to make.
In agentic coding, abstraction determines how much an AI agent needs to know to do useful work. If your codebase has clean abstractions, you can point an agent at a single module and say “implement this interface.” Without them, the agent needs to understand the whole system, which may exceed its effective context.
How It Plays Out
A team builds a payment processing system. They create a PaymentGateway interface with methods like charge and refund. Behind it, one implementation talks to Stripe, another to PayPal. The rest of the codebase only sees the interface. When a new payment provider comes along, they add a new implementation without changing anything else.
An AI agent is asked to write tests for a service that sends emails. The service depends on an EmailSender interface. Because the interface abstracts away the actual sending, the agent can write tests using a simple mock. It doesn’t need to understand SMTP, API keys, or retry logic. The abstraction makes the agent’s job tractable.
Leaky abstractions are inevitable. When performance degrades or unexpected errors surface, someone will need to look behind the curtain. Design your abstractions so that peeking behind them is possible, not forbidden.
“Create a PaymentGateway interface with charge and refund methods. Write a Stripe implementation behind it. The rest of the codebase should depend only on the interface, never on Stripe directly.”
Consequences
Good abstractions multiply productivity. They let teams work in parallel on different parts of a system, let agents operate on bounded slices of a codebase, and make code reusable across contexts.
But every abstraction is a bet that certain details won’t matter to the consumer. When that bet is wrong and the abstraction leaks, the resulting confusion can be worse than having no abstraction at all. You now have to understand both the abstraction and the reality it was hiding. The cost of a bad abstraction isn’t just complexity; it’s misleading complexity.
Related Patterns
- Uses: Interface — an interface is the visible face of an abstraction.
- Enables: Module, Component — abstraction is what makes modular design possible.
- Related to: Boundary — every abstraction implies a boundary.
- Depends on: Shape — recognizing the shape of a system helps you choose the right abstractions.
- Enables: Separation of Concerns — you separate concerns by abstracting them behind boundaries.
- Enables: Composition — composable parts rely on stable abstractions at their interfaces.
- Shields: Consumer — a consumer interacts with an abstraction rather than an implementation.
- Reduces: Coupling — abstracting away details reduces implementation coupling.
- Hides from: Dependency — abstraction lets you depend on a stable interface instead of volatile internals.
- Contrasts with: Naming — naming makes things concrete and specific; abstraction hides specifics behind a generalized interface.
Sources
- David Parnas introduced information hiding as the principle behind effective modularization in “On the Criteria To Be Used in Decomposing Systems into Modules” (Communications of the ACM, 1972). His argument that modules should hide design decisions, not simply divide work into steps, is the intellectual foundation of abstraction in software.
- Edsger Dijkstra demonstrated hierarchical layers of abstraction in practice with the THE multiprogramming system, described in “The Structure of the ‘THE’-Multiprogramming System” (Communications of the ACM, 1968). Each layer depended only on the layers below it, establishing the pattern of reasoning about complex systems one level at a time.
- Harold Abelson and Gerald Jay Sussman formalized the concept of abstraction barriers in Structure and Interpretation of Computer Programs (1984), teaching generations of programmers to build systems as towers of cleanly separated layers.
- Joel Spolsky coined the Law of Leaky Abstractions in a 2002 essay on Joel on Software, observing that all non-trivial abstractions leak some detail from the layer beneath. The article’s epigraph quotes this law directly.