skip to content

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%

answer

  1. deep = small interface, big hidden implementation (Ousterhout)
  2. shallow = interface almost as complex as the implementation
  3. interface includes ordering, threading, errors, cost model
  4. pass-through methods, information leakage, temporal decomposition
  5. network boundary raises the depth bar; expose capabilities, not CRUD

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.

solid answer

~60 s

Ousterhout's model (A Philosophy of Software Design): a module's value is functionality hidden minus interface complexity. File I/O is deep — five operations hide buffering, block allocation and device drivers. A pass-through class or a getter/setter-only wrapper is shallow: the interface is nearly as large as the implementation, so callers gain almost nothing. Consequences for boundaries: - **Minimise the number of boundaries a request crosses**, and make each one meaningful. Ten shallow layers cost more than three deep ones. - **Push complexity downward**: the module should absorb hard cases (retries, ordering, defaults) rather than exporting them as configuration and caveats. Ousterhout calls exporting them 'information leakage' and 'pass-through methods'. - **Design the interface first, from the caller's use cases**, then let the implementation be as complex as it must be. - **Beware 'temporal decomposition'** — splitting by execution order (read/process/write) usually spreads one piece of knowledge across modules; split by knowledge instead. - For services, the same test applies with higher stakes: a network boundary must hide enough to justify latency, partial failure and independent deployment.

go deeper

for a junior

Say a good module gives you a simple way to do something complicated, and that a class which just forwards calls is not pulling its weight.

for a middle

Use the benefit/cost framing, give an example of each (file I/O vs a getter/setter wrapper), and name pass-through methods as a smell.

for a senior

Add information leakage, temporal decomposition, designing the interface from use cases first, defining errors out of existence, and the higher depth bar for network boundaries.

for a principal

Tie boundaries to organisation: interfaces are team contracts, and the real return on a deep boundary is coordination avoided. Discuss the tension with leakiness (publish cost models and failure semantics), governance for escape hatches, and how to retire shallow services that pay network cost without buying independence.

## The core model John Ousterhout (*A Philosophy of Software Design*) proposes judging a module by a ratio: > **benefit = functionality hidden behind the interface** > **cost = complexity of the interface itself** (methods, parameters, ordering rules, invariants, error modes, documentation the caller must read) - **Deep module** — small interface, large hidden implementation. Examples: a file system (`open/read/write/close/seek` hiding block allocation, caching, journaling, device drivers), a garbage collector (no interface at all), a print/format routine, TCP's `send/recv`. - **Shallow module** — interface nearly as complex as what it hides. Examples: a class of getters and setters; a `Manager` that forwards to a `Service`; a wrapper whose parameters replicate the wrapped API; a 'utility' with 30 unrelated static functions. Crucially, the interface includes **everything the caller must know**: not just signatures but call ordering, thread-safety, error semantics, performance characteristics. A two-method interface with a five-paragraph 'you must call these in this order and not from this thread' is not small. ## Related failure patterns Ousterhout names - **Pass-through methods** — a method that does nothing but call another method with the same signature. It adds interface without adding functionality; the fix is to collapse or redistribute. - **Information leakage** — the same design decision (a file format, a retry policy) is embedded in several modules, so all must change together. Cure: pull the decision into one deep module. - **Temporal decomposition** — modules split by *when* things happen (read → transform → write) rather than by *what knowledge they own*, which spreads the file-format knowledge across all three. - **Configuration parameters as an excuse** — instead of solving a hard problem, the module exports a knob and makes the caller decide. Sometimes correct, often a way to push complexity upward onto many callers rather than solving it once. - **'Different layer, different abstraction'** — if adjacent layers offer the same abstraction, one of them is probably redundant. ## Applying it to boundaries **Within a codebase** 1. Start from caller use cases and design the smallest interface that serves them; only then choose the implementation. 2. Count what a caller must know to use the module correctly. If that number is close to what they'd need to know to do it themselves, collapse the module. 3. Prefer general-purpose-ish interfaces over a special-purpose method per caller — a proliferation of caller-specific methods is interface bloat and a sign the abstraction is at the wrong level. 4. Make exceptions rare (**define errors out of existence** where reasonable — e.g. a delete that succeeds when the item is absent, a substring operation that clamps rather than throws). Every exception is interface surface. **Across services** A network boundary multiplies the cost: latency, partial failure, versioning, independent deploy, on-call ownership. So the depth bar is higher. A service whose API mirrors a table's CRUD operations is the distributed version of a shallow module — callers must still orchestrate the real business behaviour, now over the network, with distributed-transaction problems added. Deep service boundaries expose business capabilities ('reserve inventory for this order') rather than data access. **Organisationally** Boundaries become team interfaces (Conway's Law). A deep boundary lets a team change everything behind it without coordination — that is the actual payoff at scale: **coordination avoided per unit of interface maintained**. A shallow boundary produces constant cross-team chatter and lock-step releases while still paying the operational cost. ## Tensions and limits - **Depth vs. flexibility.** Deep modules make strong choices; callers with unusual needs may be blocked. Mitigate with a documented escape hatch rather than by widening the main interface. - **Depth vs. leakage.** Hiding more means more that can leak (performance, failure modes). Deep modules must publish a cost model and honest error semantics — depth is not an excuse for pretending remote is local. - **Depth vs. testability.** Very deep modules can be harder to test in isolation; the answer is good internal seams, not exporting internals through the public interface. - **Classicist counterpoint.** Small-classes advice (many tiny objects) can conflict with depth. Reconcile by keeping *files* small where useful but keeping *interfaces* narrow — the enemy is exported complexity, not implementation size.

  • How does depth interact with the Law of Leaky Abstractions?
    They pull in opposite directions. The more a module hides, the more of its hidden behaviour can surprise callers through timing, failure and resource use. Deep modules therefore owe their callers an explicit cost model, honest error types and instrumentation at the boundary — depth without honesty about non-functional behaviour produces the worst leaks.
  • When is a shallow module acceptable?
    When its job is a seam rather than an abstraction: an adapter to satisfy a framework, a test double boundary, a deliberate anti-corruption layer during a migration, or a facade kept stable while internals are restructured. These are justified by a concrete, time-bounded need, and should be deleted when the need ends.
  • How would you audit an existing service landscape for shallow boundaries?
    Trace representative business operations and count service hops that add no decision; look for services whose APIs mirror table CRUD, for orchestration logic that lives in callers, for shared database access behind supposedly independent services, and for change requests that routinely require synchronised releases of two or more services. Those are shallow boundaries paying network cost for no coordination benefit.

A car's pedals are a deep interface: three controls hide combustion, transmission and braking hydraulics. A dashboard with a switch for every valve would be shallow — you would control everything and understand nothing.

saying these in an interview costs you the question

  • Judging module quality by file size or class count rather than by exported complexity
  • Believing 'more, smaller modules' is automatically better decomposition
  • Splitting modules by execution phase (read/transform/write) and calling it separation of concerns
  • Adding a configuration knob instead of solving the problem inside the module
  • Treating a CRUD-shaped microservice API as a business boundary

context