skip to content

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