skip to content

'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%

answer

  1. Wheeler's quote + Henney's addendum: except too many levels
  2. costs: comprehension, change amplification, debuggability, runtime, divergence
  3. shallow module = big interface, little hidden (Ousterhout)
  4. justify with a change needed now, not someday
  5. add on the second real use; collapsing layers is a valid refactor

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.

solid answer

~60 s

Costs of a layer, roughly in order of importance: 1. **Comprehension** — behaviour is now assembled from N files; reading a feature means N hops. Stack traces and 'go to definition' land on interfaces, not logic. 2. **Change amplification** — adding one field touches DTO, mapper, interface, impl, test double at every level. 3. **Debuggability** — the causal chain widens; dynamic dispatch, DI containers and event buses hide *who actually runs*. 4. **Runtime** — dispatch, allocation, serialisation, an extra network hop and its failure modes. 5. **Divergence** — each layer's model drifts from its neighbours, producing translation bugs. A layer earns its keep when it (a) hides substantial complexity behind a much smaller interface, (b) enables substitution you need *today* (test seam, second provider, migration strangler), or (c) enforces a boundary you must enforce (security, tenancy, module ownership). Heuristics: prefer deep modules over many thin ones; don't add a layer for a second implementation that does not exist; add indirection at the moment of the second use, not in anticipation; measure a layer by what callers stop needing to know.

code

pseudocode · 6 lines
pseudocode
// Over-indirection: four hops, zero hidden complexity
OrderController -> OrderFacade -> OrderService -> OrderManager -> OrderRepository
// each with the same method: placeOrder(order)

// Collapsed: one deep boundary that actually hides something
OrderController -> Orders.place(order)   // hides pricing, tax, stock reservation, payment, tx

go deeper

for a junior

Say that each layer means more code to read and more jumps when debugging, and that a layer should hide something real.

for a middle

List the concrete costs (comprehension, change amplification, debugging, runtime) and give the 'one implementation, no test need' smell.

for a senior

Provide a decision procedure: name the change being bought, its probability and horizon, weigh against the daily tax, prefer deep modules and just-in-time extraction; know that collapsing layers is a valid refactor.

for a principal

Talk about layer policy across an organisation — which boundaries are mandated (security, tenancy, public API, team ownership), how you keep the mandated set small, how change amplification shows up in lead-time metrics, and how architecture reviews can retire layers that no longer pay.

## The quote and its second half David Wheeler's aphorism is usually quoted only in its first half. The full form — 'except for the problem of too many levels of indirection' (Kevlin Henney's addendum) — is the important part: indirection is a **loan**. It solves today's coupling problem and charges interest forever. ## What each layer actually costs **1. Comprehension cost (the dominant one).** To understand one behaviour you must open every layer it passes through. Symptoms: 'go to definition' lands on an interface; a stack trace is 40 frames of framework; the answer to 'where does this actually happen?' takes ten minutes. John Ousterhout's term for a layer that adds interface without hiding much is a **shallow module**; a stack of shallow modules is worse than one honest deep one. **2. Change amplification.** A trivially small requirement — carry one more field end to end — touches a DTO, a mapper, an interface, an implementation, a stub, and a test at *each* level. When a one-line semantic change costs a twelve-file diff, your layering is charging more than it returns. **3. Debuggability and dynamic dispatch.** Interfaces, DI containers, plugin registries, event buses and message queues all mean the static text does not tell you what runs. That is exactly the flexibility you bought — and exactly what makes production incidents slower. **4. Runtime cost.** Usually the smallest for in-process layers (virtual calls are cheap, often devirtualised), and the largest when the indirection is a network hop: latency, retries, partial failure, serialisation, and a new thing to operate. **5. Semantic divergence.** Each layer has its own model of the domain. Translating between near-identical models is where subtle bugs live (nullability, time zones, rounding, enum drift). **6. Diffusion of responsibility.** With five layers, validation, authorisation and error mapping tend to be done partially in several of them — or in none. ## When a layer *is* worth it Require at least one of: - **Complexity hidden ≫ interface exposed.** Retry, connection pooling, protocol details, transaction management, concurrency — the caller genuinely stops thinking about something hard. - **A substitution you need now.** A test seam for time/randomness/IO; a second payment provider already contracted; a strangler boundary during a migration; a plugin surface that third parties actually implement. - **A boundary you must enforce.** Security/tenancy checks funnelled through one gate; a module boundary that keeps ownership clear; a public API you must keep stable while the internals churn. - **Rate of change asymmetry.** The layer isolates something that changes far faster (or slower) than its neighbours. ## Practical decision procedure 1. Name the change you expect the layer to make cheap, and estimate its probability and horizon. 'Someday we might switch databases' with no plan is not a justification. 2. Compare against the daily comprehension tax paid by everyone who reads the code. 3. Ask if a cheaper mechanism suffices: a named function, a parameter, a configuration value, or a compile-time abstraction rather than a runtime one. 4. Prefer **adding indirection on the second real use** (the point at which the requirement is known) over anticipating it. 5. Prefer **fewer, deeper** layers to many thin ones; collapse layers whose only job is forwarding. ## Smells of over-indirection - Interfaces with exactly one implementation and no test-double need - Layers that only rename and forward (`ServiceA` → `ServiceB` → `RepositoryC` with identical signatures) - Configuration or factories to select among options that never vary in production - 'Manager/Handler/Processor/Delegator' chains with no domain vocabulary - Needing a diagram to answer 'what happens when I click this button?' ## Removal is a legitimate design move Collapsing layers is refactoring, not regression. When a second implementation never materialised, deleting the interface and inlining the implementation usually improves both readability and performance, and is easy to reverse if the need finally appears.

  • A teammate wants an interface now 'in case we swap the database later'. How do you respond?
    Ask what the change is, how likely it is, and when. If there is no concrete plan, note that the swap will need a different seam than the one guessed today, that the daily comprehension tax is certain while the benefit is speculative, and that extracting the interface later is a mechanical refactor most IDEs automate. If a test seam is genuinely needed, say that explicitly and keep the seam minimal.
  • How do you spot over-indirection in an unfamiliar codebase quickly?
    Trace one user-visible operation end to end and count the hops that transform nothing. Look for interfaces with one implementation, files whose methods forward with identical signatures, and stack traces dominated by framework frames. High file-count-per-feature with low logic-per-file is the signature of shallow layering.
  • Is the runtime cost of indirection a good argument against it?
    Rarely, in process — virtual dispatch is cheap and often devirtualised. It matters in hot loops and, decisively, when the indirection is a network hop, where you inherit latency, partial failure and operational burden. The strong arguments are comprehension and change amplification.

Indirection is like a middleman in a supply chain: one good middleman absorbs enormous complexity; five middlemen who each just pass the order along add delay, markup and a longer phone chain when something goes wrong.

saying these in an interview costs you the question

  • Quoting only the first half of Wheeler's aphorism, as if layers were free
  • Justifying a layer solely with an unscheduled hypothetical change
  • Assuming more layers automatically means better separation of concerns
  • Treating layer removal as regression rather than refactoring
  • Measuring design quality by number of abstractions rather than by what callers can ignore

context