"Any problem can be solved by adding a level of indirection — except too many levels of indirection." How do you decide when an indirection is worth adding, and what does an unjustified one cost?
answer
- one-sentence test: 'so X can change without touching Y'
- rule of three before extracting
- wrong abstraction > duplication in cost
- does the layer add or just forward?
- count hops per request
basics
~20 sAdd it when you can name what it decouples: a real second implementation, an unstable external dependency, a test seam, or a place to put cross-cutting concerns. Otherwise you pay extra hops, harder debugging and more code for imagined flexibility.
solid answer
~50 sAn indirection is earned when you can state the coupling it removes in one sentence and point at evidence: a second implementation that exists or is scheduled; an external system you don't control; a boundary you must cross for tests, transactions, retries, authorization or metrics; a dependency cycle to break; or an org boundary where two teams need independent release. Unjustified indirection costs are real and compounding: every request crosses more hops, so stack traces and change-impact analysis get longer; "go to definition" lands on an interface instead of behaviour; each layer needs its own tests and DTO mapping; runtime indirection adds latency and failure modes; and a wrong abstraction is stickier than duplication because callers now depend on it. Rule of thumb: prefer the *reversible* choice. Inlining a needless indirection later is cheap; unwinding a wrong abstraction that fifty callers depend on is not. Rule of three — extract when the third case arrives, not the first.
go deeper
Say it plainly: add the middle object when there is a real reason (external system, testing, a second implementation); otherwise it is extra code and an extra place to look when debugging.
Give the one-sentence test and the rule of three, and name concrete costs: more files per change, DTO mapping, longer traces.
Balance the ledger explicitly, cite 'duplication is cheaper than the wrong abstraction', and discuss over-mocked tests and pass-through layers as detectable symptoms.
Frame it as reversibility and cost-of-change economics across a system and an org — which boundaries are one-way doors (public APIs, storage formats, cross-team contracts) and therefore deserve early indirection, versus internal seams that should stay concrete until evidence arrives.
## The aphorism The first half — *"All problems in computer science can be solved by another level of indirection"* — is attributed to David Wheeler and popularized by Butler Lampson. The second half — *"…except the problem of too many levels of indirection"* (Kevlin Henney's addition) — is the part senior engineers are actually tested on. Indirection is the GRASP principle of putting an intermediate object between two components so they stay decoupled; it is genuinely powerful and genuinely not free. ## The ledger ### Benefits (each must be *claimed specifically*) 1. **Substitutability** — a second implementation exists (vendor B, in-memory store, feature-flagged rollout). 2. **Change containment** — the far side is outside your control and changes on its own schedule (third-party API, another team's service, a wire format). 3. **Test seam** — the caller cannot be unit-tested without cutting here (network, clock, filesystem, randomness, payments). 4. **Cross-cutting hook** — one place to add retries, timeouts, circuit breakers, caching, authorization, tracing, transactions. 5. **Cycle breaking** — A and B need each other; both depending on M turns a cycle into a DAG. 6. **Mesh collapsing** — N peers × N references becomes N × 1 via a hub. 7. **Vocabulary protection** — foreign concepts (HTTP codes, ORM entities, protobuf types) stop at the boundary instead of leaking through the domain. 8. **Organizational boundary** — two teams need a stable contract to release independently (Conway's law made explicit). ### Costs (paid whether or not the benefit materializes) 1. **Traceability.** Every hop is another file to open when reading a bug. "Go to definition" lands on an interface; you need "go to implementation", and with dependency injection the wiring may be invisible in source. 2. **Cognitive load.** Understanding a feature now requires holding several vocabularies at once. 3. **Mapping tax.** Layers usually bring DTOs and mappers; a field addition becomes an edit in four files. 4. **Test surface.** Each layer wants its own tests; over-mocked tests then verify the *wiring* rather than the *behaviour*, and pass while the system is broken. 5. **Runtime cost.** If the indirection is a network hop, a queue, or a serialization boundary, you buy latency, partial failure, ordering questions, and new operational alarms. 6. **Abstraction lock-in.** Once callers depend on your interface, changing it is a coordinated migration. Sandi Metz: *"duplication is far cheaper than the wrong abstraction."* 7. **False safety.** A leaky mediator (vendor types in the signatures, `Any`/`Map<String,Object>` escape hatches, provider-specific error codes) gives you the cost with none of the containment. ## Decision heuristics **The one-sentence test.** "This exists so that X can change without touching Y." If you can't fill in X and Y with concrete names, don't add it. **Rule of three.** Extract the abstraction when the third concrete case shows up. With one case you are guessing at the axis of variation; with two you may be pattern-matching on coincidence. **Reversibility (one-way vs two-way doors).** Adding an indirection later, when the second implementation actually arrives, is a mechanical refactor. Removing one that fifty call sites and a public contract depend on is a project. Prefer the cheap-to-reverse direction: start concrete. **YAGNI vs the exception.** "You aren't gonna need it" is the default; the honest exceptions are (a) crossing a boundary you don't own, and (b) boundaries that are expensive to introduce later — public APIs, persistence formats, cross-team contracts, anything with data migration attached. **Does the layer add or forward?** If every method forwards 1:1 with the same types and no policy, the layer is a pass-through. Delete it or give it a job. **Who owns the contract?** A good indirection's interface is written in *your* vocabulary. If it reads like the vendor's SDK, you built a rename, not a boundary. **Count the hops per request.** A useful architecture review metric: for one representative use case, how many objects does control pass through, and what does each contribute? Hops that contribute nothing are the budget you can reclaim. ## Worked examples **Justified.** A payments integration behind `PaymentGateway`: the vendor is external (change containment), tests must not hit the network (test seam), retries/idempotency live in one place (cross-cutting), and a second provider is a plausible commercial requirement (substitutability). Four benefits, one hop. **Unjustified.** `IUserService` with a single implementation `UserService`, methods identical, existing "so we can mock it". Modern test tooling can fake concrete classes; the second implementation never came. Cost: two files, an indirection in every trace, zero decoupling. **Actively harmful.** A repository interface whose methods take the ORM's criteria objects and return ORM entities. Callers still depend on the ORM, the interface cannot be implemented by anything else, and swapping technologies still touches everything — while the team believes it is protected. **Cost paid deliberately.** An anti-corruption layer between a modern domain and a legacy mainframe: verbose, duplicative, and correct — because the alternative is legacy concepts colonizing the new model. ## What good looks like in an interview answer Don't argue "layers good" or "layers bad". Show that you price the hop: name the specific benefit claimed, name the specific cost paid, note that the decision is easier to reverse in one direction than the other, and give a case where you would deliberately accept a costly indirection anyway.
- Why is 'we might swap the database someday' usually a weak justification for a repository layer?Because the swap almost never happens, and when it does, the interface designed against one engine's semantics (transactions, isolation, query capability, ID generation) rarely survives contact with another. The strong justifications are different: keeping the domain free of infrastructure vocabulary and providing a fast test seam — both of which pay off on day one.
- How can over-indirection make tests worse rather than better?Every hop invites mocking the next hop. The suite ends up asserting that A called B with certain arguments, which is a restatement of the implementation. Such tests break on harmless refactors and pass while the integrated behaviour is broken, because no test ever exercises two real layers together.
- Is 'duplication is cheaper than the wrong abstraction' an argument against indirection?No — it is an argument about timing. It says: tolerate duplication until the shared axis of change is demonstrated, because callers that depend on a premature abstraction make it expensive to correct. Once the axis is real, the indirection is the cheap option.
Middle managers. One with a real job — translating between two groups that genuinely can't talk directly — earns their seat. A chain of five who only forward messages slows every decision, and nobody can tell you where the decision was actually made.
saying these in an interview costs you the question
- "Always code to an interface" applied mechanically — an interface with one implementation that mirrors it method-for-method decouples nothing.
- Justifying a layer with 'we might need to swap X someday' without a concrete second implementation or an owner asking for it.
- Assuming indirection is free at runtime — a hop that crosses a process, queue, or serialization boundary adds latency and new failure modes.
- Believing indirection removes coupling rather than relocating it onto a contract you now own and must version.
- Treating a leaky mediator (vendor types in its signatures) as protection — it is cost without containment.
- Ignoring reversibility: adding indirection later is usually cheap, removing an entrenched one is not.