How does the Indirection principle scale from a single mediating class to architectural constructs such as an anti-corruption layer, ports-and-adapters, an API gateway, or a message broker — and what changes when the indirection becomes a network hop?
answer
- same principle, different cost curve
- ACL = translation as defence (DDD)
- ports/adapters = dependency inversion
- network hop = partial failure + versioning + on-call
- ESB anti-pattern: mediator accretes business logic
basics
~20 sThe idea is the same at every scale: put something in the middle so two sides don't couple. But once the middle is a separate process, you also inherit latency, partial failure, versioning, deployment and ownership — problems a method call never had.
solid answer
~50 sIn-process, Indirection costs a method call and a file to open. At architectural scale the same principle appears as an **anti-corruption layer** (translating another bounded context's model so it can't colonize yours), **ports-and-adapters/hexagonal** (the domain defines ports; infrastructure supplies adapters, inverting the dependency), an **API gateway or BFF** (one edge mediating many services and clients), a **message broker or event bus** (temporal decoupling — producers don't know consumers), and a **service mesh sidecar** (retries, mTLS, tracing hoisted out of application code). What changes when the middle becomes a network hop: latency and tail-latency amplification; partial failure and the need for timeouts, retries with idempotency, and circuit breakers; distributed-tracing requirements to keep the system debuggable; independent versioning and compatibility rules for the contract; capacity, deployment and on-call ownership; and the risk of a shared middle becoming a bottleneck, a single point of failure, or a place where business logic quietly accumulates (the ESB anti-pattern). The principle is unchanged; the cost curve is not.
go deeper
Recognize that the same 'put something in the middle' idea appears at bigger scales — gateways, queues — and that a network hop is much more expensive than a method call.
Name the constructs (ACL, ports and adapters, gateway, broker) and one concrete consequence of the hop: retries need idempotency, traces need correlation IDs.
Reason about the trade curve: latency and tail amplification, partial failure handling, contract versioning across independent deploys, and observability as a prerequisite for decoupling.
Treat boundaries as organizational and economic commitments: named benefit, owning team, explicit rules about what the mediator may not contain, an observability budget, and an honest assessment of reversibility before the boundary is drawn.
## Same principle, five orders of magnitude GRASP's **Indirection** — *assign to an intermediate object the responsibility of mediating between two components so they remain decoupled* — is stated for objects, but it is scale-free. The same sentence describes a virtual method table, a repository class, an anti-corruption layer, a message broker, DNS, and a load balancer. What is *not* scale-free is the cost function. ### The ladder | Scale | Construct | What it mediates | Marginal cost | |---|---|---|---| | Language runtime | virtual dispatch, interfaces, DI container | caller ↔ implementation | ~free | | Class | adapter, mediator, controller, repository | object ↔ object | a file, a hop | | Module | port + adapter (hexagonal), facade over a package | domain ↔ infrastructure | a contract to maintain | | Bounded context | **anti-corruption layer (ACL)** | your model ↔ their model | a translation layer, deliberate duplication | | Deployment | API gateway / BFF, message broker, service mesh sidecar, proxy | service ↔ service, client ↔ system | a process: latency, failure, ops, ownership | | Internet | DNS, CDN, load balancer | name ↔ address, client ↔ origin | operated by someone else | ### Anti-corruption layer From Domain-Driven Design. When your bounded context must consume another one — a legacy system, a vendor, another team's service — a direct integration lets *their* model dictate *your* model: their identifiers, their state machine, their nulls, their idea of a "customer". An ACL is an indirection whose sole responsibility is **translation and defence**: it maps their concepts into yours and refuses to let theirs escape. It is deliberately verbose and deliberately duplicative — that verbosity *is* the product. It is the clearest case where you accept a high indirection cost knowingly, because the alternative (model corruption) is unbounded. Contrast with the other DDD context-mapping relationships: *Conformist* (accept their model, no indirection, cheapest, no protection), *Shared Kernel* (co-own a model, tight coupling by agreement), *Published Language* (both sides translate to a neutral, versioned contract). Choosing among them is precisely the decision "how much indirection do we buy at this boundary, and who pays for it." ### Ports and adapters (hexagonal / clean architecture) The domain declares **ports** — interfaces expressed in its own vocabulary (`OrderRepository`, `NotificationSender`). Infrastructure supplies **adapters** implementing them (Postgres, SMTP, Kafka). The key move is **dependency inversion**: the arrow points from infrastructure to domain, not the reverse, so the domain compiles and tests without any infrastructure present. Two failure modes: (a) ports drafted from the technology's shape rather than the domain's, which is a rename, not an inversion; (b) so many ports and DTO mappings that ordinary features cost four files. ### API gateway and backend-for-frontend One mediator at the edge for many clients and many services: routing, authentication, rate limiting, aggregation, protocol translation, response shaping per client type. Benefit: clients don't know the service topology, so services can split, merge, or move without client releases. Risks: a monolith at the edge, a deployment bottleneck for every team, and — if aggregation logic grows teeth — business rules living in a component owned by nobody in particular. ### Message broker / event bus Indirection in *time* as well as space. Producers publish; consumers subscribe; neither knows the other, and neither needs to be up simultaneously. Buys elasticity, fan-out, and buffering. Costs: at-least-once delivery (so consumers must be idempotent), ordering guarantees that are weaker than they look, poison messages and dead-letter handling, schema evolution across independently deployed consumers, and the loss of a straightforward end-to-end stack trace — the trace must be reconstructed via correlation IDs. The historical failure mode is the **enterprise service bus** anti-pattern: the intermediary accretes routing rules, transformation logic, and eventually business logic, becoming a centralized, hard-to-test, hard-to-deploy chokepoint. Modern practice pushes for "dumb pipes, smart endpoints" for exactly this reason. ### Service mesh sidecar A proxy deployed next to each service instance that intercepts all traffic to apply retries, timeouts, mTLS, load balancing, and tracing. It is indirection used as a **cross-cutting hook** hoisted out of application code and into the platform, so policies are uniform and changeable without recompiling services. Costs: an extra process per instance (memory, latency, another thing to upgrade), and a whole new control plane to operate and debug. ## What genuinely changes at a process boundary 1. **Latency and tail amplification.** A hop adds a median cost and, worse, a tail. A request crossing several mediated hops has a p99 dominated by the worst hop; fan-out multiplies the chance of hitting a slow one. 2. **Partial failure.** A method call cannot half-succeed; a network call can time out after the far side committed. This forces timeouts, bounded retries, **idempotency keys**, circuit breakers, and backpressure — none of which in-process indirection needs. 3. **Debuggability.** The stack trace stops at the boundary. You must invest in correlation IDs and distributed tracing, or the indirection becomes an epistemic wall during incidents. 4. **Versioning.** In-process, caller and callee ship together. Across a boundary they don't: you need backward/forward-compatible schemas, expand-then-contract migrations, and a deprecation policy. The contract becomes a long-lived asset with an owner. 5. **Ownership and on-call.** A shared mediator needs a team. "Everyone's gateway" reliably becomes no one's gateway, and the queue for changes becomes an organizational bottleneck. 6. **Capacity and blast radius.** A shared middle is a potential single point of failure and a shared saturation point; its failure modes are correlated across all consumers. 7. **Security posture.** Every hop is a trust boundary: authentication between hops, authorization decisions that must not be silently re-derived, and a decision about whether the mediator is inside or outside the trust perimeter. ## The principal-level judgement The question at architectural scale is never "is indirection good" but **"what is this boundary buying, who owns it, and what is the exit?"** A useful checklist: - **Named benefit.** Independent deployability? Protection from a foreign model? Uniform cross-cutting policy? Temporal decoupling? Multi-client shaping? One of these, named. - **Alignment with organization.** Boundaries that don't match team ownership create coordination cost without autonomy benefit (Conway's law, and its inverse manoeuvre). - **Logic containment.** Write down what the mediator may *not* contain. Every long-lived intermediary drifts toward holding business logic; the ESB is the cautionary tale. - **Observability budget.** If you cannot trace across it, you have bought decoupling with debuggability. - **Reversibility.** Removing a class-level indirection is an afternoon. Removing a broker that a dozen teams publish to is a program of work. Price boundaries by how hard they are to undo, not by how elegant the diagram looks. - **Layer count discipline.** Ask how many mediated hops a representative request crosses and what each contributes. Hops that contribute nothing are pure tax — and at network scale, taxed in latency and incidents, not just in files.
- What is the anti-corruption layer protecting against, concretely?Model corruption: the other context's identifiers, state machine, naming, nullability, and error semantics propagating into your domain until your model is a mirror of theirs. The ACL translates at the boundary and is deliberately duplicative — the duplication is the price of keeping your model coherent and independently evolvable.
- Why did the enterprise service bus become an anti-pattern if it is 'just' indirection?Because a long-lived shared intermediary attracts responsibility. Routing became transformation, transformation became orchestration, orchestration became business logic — in a centrally owned component that every team had to queue behind and that no team could test end to end. Hence 'smart endpoints, dumb pipes'.
- What must consumers do differently because a broker typically delivers at-least-once rather than exactly-once?Be idempotent: deduplicate on a message or business key, make handlers safe to re-run, and treat ordering as a weaker guarantee than it appears (usually per-partition at best). They also need dead-letter handling for poison messages and a schema-evolution policy, since producers and consumers deploy independently.
- How do you keep a system debuggable once indirection crosses process boundaries?Propagate a correlation/trace ID across every hop, emit spans with consistent naming, and make the mediator itself a first-class tracing participant. Without that, the stack trace ends at the boundary and every incident becomes archaeology across several teams' logs.
A doorway between two rooms versus a customs border between two countries. Both are 'something in the middle so the sides stay separate', but the border needs staff, hours of operation, paperwork versions, a queue when it's busy, and a plan for what happens when it closes.
saying these in an interview costs you the question
- "Microservices are just indirection, so more services means better decoupling" — service boundaries buy independent deployment but cost partial failure, versioning, and ops.
- Treating a broker as if it delivered exactly once and in strict global order — real systems are at-least-once with, at best, partitioned ordering.
- Building an anti-corruption layer and then letting the foreign model's types pass straight through it — that is cost with no protection.
- Ports drafted from the database or framework's shape rather than the domain's vocabulary — dependency inversion in diagram only.
- Assuming an API gateway is free — it is a shared deployment bottleneck, a single point of failure, and a magnet for logic that belongs in services.
- Ignoring ownership: a shared mediator with no owning team becomes a change queue and an on-call orphan.
- Forgetting that each network hop is also a trust boundary requiring its own authentication and authorization decisions.