skip to content

Abstraction and Indirection

Choosing the right level of abstraction, and knowing that another layer of indirection solves most problems except too many layers of indirection. You will learn what makes an abstraction leak and how to judge when a decoupling layer is earning its keep.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In software design, what is the difference between abstraction and indirection, and why is it said that every abstraction introduces indirection but not every indirection is an abstraction?

level: juniorimportance: must knowfreq 55%

answer

  1. indirection = mechanical hop; abstraction = simpler mental model
  2. abstraction ⇒ indirection, never the reverse
  3. pass-through interface = layer, not abstraction
  4. test: does the caller now think in fewer terms?
  5. deep module = small interface, large hidden functionality

basics

~20 s

Abstraction hides detail behind a simpler idea you can reason about, like 'send a payment'. Indirection just means going through something in between instead of calling the thing directly. Abstractions use indirection, but a pass-through wrapper that hides nothing is only indirection.

solid answer

~50 s

Indirection is a mechanical property: caller A no longer references B directly, it goes through C — an interface, a pointer, a name lookup, a proxy, a queue, a URL. It buys the ability to swap, intercept or defer what sits behind C. Abstraction is a semantic property: the caller is handed a smaller, more meaningful vocabulary than the underlying mechanism, and genuinely does not need the hidden details to use it correctly. A file handle, a map/dictionary, an HTTP endpoint, sort(list) — each replaces many details with one concept. Abstraction is normally implemented with indirection, so you get indirection for free. The reverse fails: an interface whose methods mirror one implementation one-for-one adds a hop and a file but no new concept — indirection without abstraction. That is the classic waste: you pay navigation, runtime and cognitive cost without gaining simpler reasoning or substitutability. Test: after the change, does the caller think in fewer, better terms? If not, you added a layer, not an abstraction.

code

pseudocode · 8 lines
pseudocode
// Indirection without abstraction — same vocabulary, one more hop
interface UserRepo { findById(id); insert(row); updateRow(row) }
class SqlUserRepo implements UserRepo { /* mirrors the SQL 1:1 */ }

// Abstraction — caller loses details it no longer needs
interface Accounts {
  register(email, password): UserId   // hides hashing, uniqueness check, tx, events
}

go deeper

for a junior

State the two definitions plainly and give one example of each; note that a wrapper that just forwards calls is indirection only.

for a middle

Add the cost side — navigation, cognitive load, sometimes dispatch — and the test 'does the caller's vocabulary shrink?'

for a senior

Bring in deep vs shallow modules (interface size vs functionality hidden), name the cases where pure indirection is still justified (seams, proxies, DI), and mention that compile-time abstraction can be free at runtime.

for a principal

Frame it economically: indirection is buying an option on future change; abstraction is buying reduced coupling of understanding. Discuss when an org should pay for options it may never exercise, and how abstraction boundaries become team boundaries.

## Two words that get used interchangeably **Indirection** — you refer to something that *stands for* the real thing, and the real thing is resolved later or elsewhere. Examples at every level of a system: - a pointer/reference (an address, not the value) - a variable name resolved by a symbol table - an interface/virtual method dispatched at runtime to whatever object you were given - a DNS name resolved to an IP; a load balancer in front of servers - a message queue between producer and consumer - a feature flag deciding which code path runs What indirection buys is always the same shape: **the binding between caller and callee is made late, so it can be changed without changing the caller.** Swap the implementation, intercept the call (logging, retries, caching, auth), fan out to many targets, or move the callee to another machine. **Abstraction** — you offer a *simplified model* that is correct and usable on its own terms. The key phrase is *you no longer need to know*. A caller of `sort(list)` need not know it is a hybrid mergesort; a caller of a `Map` need not know about hashing or tree buckets; a caller of TCP need not know about retransmission. Abstraction is measured by what the reader can stop thinking about. ## Why the asymmetry To hide a detail you must stop naming it directly — so you introduce a name, interface or function that stands in for it. That *is* indirection. Hence: abstraction ⇒ indirection. But indirection alone hides nothing. A wrapper that exposes the same operations, same parameters and same failure modes as the thing it wraps has moved the call site without shrinking anyone's mental model. You now pay: - **Navigation cost** — one more jump to understand the code - **Cognitive cost** — a second name for the same idea - **Sometimes runtime cost** — dispatch, allocation, network hop and gain only the *option* of substitution, which is worthless if you never exercise it. ## How to tell them apart in review Ask three questions: 1. **Vocabulary** — does the caller now use domain words instead of mechanism words? (`chargeCard(order)` vs `postJson(url, body, headers)`) 2. **Ignorance** — can I use it correctly having read only its signature and doc, never its implementation? 3. **Ratio** — is the amount of functionality hidden large relative to the size of the interface exposed? (John Ousterhout calls a good ratio a *deep* module and a bad one a *shallow* module.) If all three fail, what you have is a layer, not an abstraction. ## Edge cases worth knowing - **Deliberate indirection with no abstraction is sometimes right**: a seam for testing, a proxy for instrumentation, a dependency-injection point, an indirection you add *the moment* the second implementation appears. - **Abstraction without runtime indirection exists**: compile-time generics, inlined functions, type aliases. The indirection is conceptual and erased by the compiler — you still can't see the details, but there is no dispatch cost. - **Naming is the cheapest abstraction**: extracting a well-named function with no interface at all often gives most of the benefit at almost no cost.

  • Give a case where you would add indirection even though it adds no abstraction.
    A seam you need right now: a clock/time provider so tests can control time, a proxy that adds retries and metrics uniformly, or a dependency-injection boundary that lets a test substitute a fake. The justification is a concrete capability today, not a hypothetical second implementation someday.
  • Does adding an interface always cost runtime performance?
    No. Compile-time mechanisms (generics, inlining, monomorphisation) and JIT devirtualisation often erase the cost when there is one implementation. The reliable cost is human: an extra hop to read, an extra name to learn, and a place where behaviour can diverge.

A light switch is an abstraction: 'on/off' replaces mains wiring, current and circuit breakers. An extension cord is indirection: the lamp is still exactly as complicated, the plug just moved.

saying these in an interview costs you the question

  • Claiming an interface is an abstraction purely because it is an interface
  • 'Always code to an interface' applied to every class, producing Foo/FooImpl pairs
  • Believing abstraction is about hiding code rather than about what the caller no longer needs to know
  • Assuming indirection is free at runtime, or conversely that it is always expensive
  • Confusing encapsulation (hiding state) with abstraction (offering a simpler model)

context

open as a page

What is a 'leaky abstraction' (Joel Spolsky's Law of Leaky Abstractions), and how should it change the way you design and consume abstractions?

level: middleimportance: must knowfreq 50%

basics

~20 s

A leaky abstraction is one whose hidden details still show through — usually as performance surprises or unusual errors — so you must understand what is underneath to use it correctly. The law says all non-trivial abstractions leak to some degree.

open as a page

'Every problem in computer science can be solved by another level of indirection — except the problem of too many levels of indirection.' What concrete costs does each extra layer impose, and how do you judge whether a new one earns its keep?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Each layer adds code to read, a jump when tracing a bug, another place behaviour can differ, and sometimes runtime cost. A layer earns its place when it hides real complexity or enables a change you actually need now — not a hypothetical one.

open as a page

How do you decide when to extract an abstraction from duplicated code versus leaving the duplication in place, and what is meant by 'a wrong abstraction is more expensive than duplication'?

level: middleimportance: should knowfreq 40%

basics

~20 s

Extract when the pieces are the same idea, not merely the same text, and when they change together for the same reason. If they only look alike, keep the duplication — a shared abstraction forced to serve two different reasons grows flags and branches and becomes harder to remove than the copies were.

open as a page

What makes an abstraction 'deep' rather than 'shallow' in John Ousterhout's sense (interface cost versus functionality hidden), and how would you apply that idea when drawing module or service boundaries?

level: principalimportance: should knowfreq 30%

basics

~20 s

A deep module offers a small, simple interface but hides a lot of work; a shallow one exposes almost as much complexity as it contains. Prefer fewer, deeper boundaries — each interface is a cost paid by every caller.

open as a page

When is it worth putting your own interface (for example an anti-corruption layer) in front of a third-party library or external service, and when is that wrapper just ceremony?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Wrap when the dependency is likely to change, imposes its types on your core domain, or is hard to test against. Skip the wrapper when the dependency is stable, ubiquitous and already speaks a good vocabulary — a wrapper that mirrors its API buys nothing.

open as a page