skip to content

At architecture scale, where do Facade, Proxy, Adapter and Decorator reappear as system-level building blocks, and what do you weigh before adding another layer of indirection?

level: principalimportance: should knowfreq 34%

answer

  1. intents are scale-free: class → module → service
  2. gateway=Facade, sidecar=Proxy, ACL=Adapter, middleware=Decorator
  3. each hop: latency, failure mode, trace, owner
  4. facade must be narrower and coarser, not 1:1
  5. name the change it absorbs, or don't add it

basics

~20 s

The same intents scale up: an API gateway or service facade simplifies a subsystem, RPC client stubs and sidecars are proxies, anti-corruption layers are adapters, and middleware chains are decorators. Each layer buys decoupling but costs latency, one more hop to debug, and a contract someone must own.

solid answer

~50 s

Structural intents are scale-free. Facade appears as a coarse-grained service API, a backend-for-frontend, or a module's single public entry point that hides internal collaborators. Proxy appears as RPC stubs, service-mesh sidecars, reverse proxies, caching and authorization gateways — controlled access to something remote, expensive or restricted. Adapter appears as an anti-corruption layer around a vendor or legacy system, translating its model into your own. Decorator appears as middleware and resilience chains: logging, tracing, retry, timeout, circuit breaker composed around a call. What changes with scale is the cost profile: an in-process wrapper costs a stack frame, a network-level one costs latency, a failure mode, deployment surface and on-call ownership. Before adding a layer I want a named second implementation or a real change that the layer absorbs, agreement on who owns its contract, and a plan for observability across the hop; otherwise the indirection is speculative and permanent.

go deeper

for a junior

Map each in-process pattern to one system-scale example (gateway, client stub, legacy wrapper, middleware) and note that layers add indirection.

for a middle

Explain what each layer buys (a stable seam, a home for cross-cutting concerns) and name the direct costs: latency, extra failure modes, more code to trace.

for a senior

Discuss concrete failure patterns — pass-through facades, leaky anti-corruption layers, middleware ordering, retry amplification — and where to place translation versus policy.

for a principal

Argue from ownership, evolution and operations: what change the layer absorbs, in-process versus out-of-process, contract versioning and deprecation, observability across hops, and explicit removal criteria so layers do not accumulate.

## The claim: these intents are scale-free GoF described patterns for classes in one process, but the intents — convert an interface, control access, simplify a subsystem, attach responsibilities — are about *composition*, not about language constructs. They reappear at module, service and network scale, which is why the vocabulary is still useful in architecture reviews. | Intent | In-process form | System-scale form | |---|---|---| | Simplified entry to a subsystem (**Facade**) | one public `*Api` class hiding a module's internals | API gateway, backend-for-frontend, coarse service API over several internal services | | Controlled access (**Proxy**) | lazy loader, protection wrapper | RPC/client stub, service-mesh sidecar, reverse proxy, CDN edge cache, authorization gateway | | Interface conversion (**Adapter**) | wrapper translating a vendor SDK | anti-corruption layer around a legacy or vendor system; protocol translation (SOAP↔REST, on-prem↔cloud) | | Attached responsibilities (**Decorator**) | retry/log wrapper around a client | middleware/filter chain, sidecar interceptors, resilience stacks | | Part-whole uniformity (**Composite**) | UI/document tree | nested resource hierarchies (org → project → resource), aggregating APIs that compose sub-responses | | Two independent axes (**Bridge**) | abstraction + implementor hierarchies | hexagonal ports with swappable infrastructure; the same domain over multiple storage or cloud providers | | Shared fine-grained state (**Flyweight**) | interned values | shared reference-data caches, deduplicated content-addressed storage | ## What you gain - **A stable seam.** Callers depend on your interface, not on the thing behind it, so the thing behind it can be replaced, moved, scaled or bought. - **A place to put cross-cutting concerns.** Auth, retries, tracing, rate limits, translation, caching all need *somewhere*; a named layer keeps them out of business logic. - **A blast-radius boundary.** A facade or anti-corruption layer contains the spread of an external model change to one component. - **Independent evolution.** Two teams can move at different speeds on either side of a contract. ## What you pay — the part people skip 1. **Latency and failure modes.** An in-process wrapper adds nanoseconds; a network hop adds milliseconds plus a new set of ways to fail (timeout, partial failure, retry storms, head-of-line blocking). Every hop needs its own timeout and retry budget, and nested retries multiply load exponentially unless budgeted. 2. **Debuggability.** One more component in every trace, one more log stream to correlate, one more place a request can be silently dropped or transformed. Distributed tracing is not optional once you have several hops. 3. **Ownership.** A contract needs an owner, a versioning policy and a deprecation path. An unowned layer becomes a dumping ground. 4. **Leaky facades.** The classic failure: a facade that mirrors the subsystem one-to-one adds a hop and no simplification, and every subsystem change ripples straight through it. A facade must be *narrower* and *coarser* than what it hides, expressed in the caller's language, or it is not a facade. 5. **Speculative generality.** A layer added for a hypothetical second implementation is permanent cost against uncertain benefit. Demand a named, dated second case. 6. **Consistency and caching.** Proxies and caches introduce staleness and invalidation; that is a correctness concern, not a performance detail. 7. **Cognitive load.** Every layer is a file or a service a newcomer must learn to trace a request end to end. ## The decision checklist I actually use - **What concrete change does this layer absorb?** Name it: "we are migrating from vendor X to Y in Q3", "three clients need different shapes of the same data", "authorization must not be reimplemented per service". If the answer is "future flexibility", stop. - **Is it a translation boundary or a policy boundary?** Translation (Adapter/Facade) belongs at the edge nearest the foreign model. Policy (Proxy — authz, quota, retries) belongs where it can be enforced uniformly, which usually means infrastructure rather than a library each team must adopt. - **In-process or out-of-process?** Prefer in-process (a class, a module boundary) until you need independent deployment, independent scaling, or language heterogeneity. Turning a class into a service multiplies the cost of the same intent by an order of magnitude. - **Who owns the contract, and how does it version?** - **Can I observe through it?** Correlation IDs propagated, timeouts and retry budgets defined per hop, errors mapped rather than swallowed. - **What is the removal plan?** Layers accumulate. A layer with no exit criteria is forever. ## Sharper failure patterns worth naming - **Pass-through facade / gateway that grows business logic** — starts as routing, becomes a distributed monolith's hidden core. - **Adapter that leaks the foreign model** — the anti-corruption layer passes vendor DTOs straight through, so the corruption arrives anyway with an extra hop. - **Decorator chains nobody can order** — retries wrapped outside timeouts (or vice versa) produce very different behaviour; correctness of a resilience stack depends on composition order, which lives in wiring code no one reviews. - **Proxy that hides remoteness too well** — the fallacies of distributed computing: making a network call look like a local one invites callers to treat it as free, reliable and ordered. ## The honest summary Structural patterns are how you buy the ability to change one thing without changing another. That ability is real and often worth it. The bill arrives as latency, operational surface and comprehension cost, and it is paid every day by people who did not make the decision. Add the layer when you can name the change it absorbs and the person who owns it; otherwise you have added a hop.

  • How do you tell a genuine facade from a pass-through layer?
    Compare the interfaces. A genuine facade is narrower and coarser than what it hides, is phrased in the caller's vocabulary, and absorbs changes behind it. A pass-through mirrors the subsystem method-for-method, so every internal change ripples to callers and the layer contributes only a hop.
  • Why is composition order dangerous in a resilience middleware chain?
    Because semantics change with order. Retry outside timeout retries a whole timed-out operation; timeout outside retry bounds all attempts together. Circuit breaker inside retry never opens fast enough; outside retry it can trip on retried failures. The effective behaviour lives in wiring code, not in any single component, so it is easy to get wrong and hard to review.
  • When should a cross-cutting concern be a library wrapper versus infrastructure (sidecar, gateway)?
    Library wrappers keep latency low and give type-safe access to application context, but every service must adopt and upgrade them, which is hard across languages. Infrastructure enforces uniformly regardless of language and can be upgraded centrally, at the cost of a hop and less application context. Policy that must be guaranteed (authz, quotas, mTLS) leans infrastructure; concerns needing domain context lean library.

Adding a layer is like adding a middle manager. Done well, one person absorbs churn so two groups can work independently. Done badly, you have added a meeting, a translation error and someone to page at 3am, while the two groups still coordinate directly.

saying these in an interview costs you the question

  • Treating patterns as automatically good — 'we added a facade' is not by itself an improvement.
  • Adding a layer for hypothetical future flexibility with no named second implementation.
  • Facades that mirror the subsystem one-to-one and therefore simplify nothing.
  • Ignoring that a network-level wrapper introduces new failure modes, not just latency.
  • Nested retries without a shared retry budget, causing amplification under load.
  • Assuming a remote proxy makes distribution transparent — it hides latency and partial failure until production.
  • No named owner or versioning policy for a new contract.

context