skip to content

Adapter, Decorator and Proxy all hold a reference to another object and forward calls to it. How do their intents differ, and how do you decide which one you are actually building?

level: middleimportance: must knowfreq 84%

answer

  1. Adapter = different interface
  2. Decorator = same interface, stackable, opt-in
  3. Proxy = same interface, gatekeeper of access
  4. virtual / remote / protection / smart reference
  5. decorator order matters: compress before encrypt

basics

~20 s

Adapter changes an object's interface so an existing client can use it. Decorator keeps the same interface and adds behaviour, and decorators can be stacked. Proxy also keeps the same interface but controls access — lazily creating, checking permissions, caching or reaching a remote object.

solid answer

~50 s

All three are wrappers; the discriminator is why the wrapper exists and whose interface it exposes. Adapter solves an incompatibility you did not choose: the client expects interface T, you have a component with interface S, so the adapter implements T and translates to S. The wrapped object's interface is deliberately different from the exposed one. Decorator exposes exactly the wrapped object's interface and adds responsibilities to one instance at runtime; because in and out types match, decorators compose into chains (buffering, compression, encryption over a stream). Proxy also exposes the same interface, but the extra work is about access rather than functionality: virtual proxy defers expensive creation, remote proxy hides network transport, protection proxy enforces authorization, smart reference counts or locks. Practical test: interface changes → Adapter; same interface, stackable feature the caller opts into → Decorator; same interface, single mandatory gatekeeper the caller cannot see → Proxy.

code

pseudocode · 25 lines
pseudocode
// Same shape, three different reasons.

// ADAPTER: exposed interface != wrapped interface
class CelsiusSensorAdapter implements TemperatureSensor {   // client wants Fahrenheit API
  constructor(private celsius: LegacyCelsiusProbe) {}
  readFahrenheit() { return this.celsius.read() * 9 / 5 + 32 }
}

// DECORATOR: same interface, stackable, opt-in feature
class GzipStream implements Stream {
  constructor(private inner: Stream) {}
  write(bytes) { this.inner.write(gzip(bytes)) }
}
// usage: new GzipStream(new BufferedStream(new FileStream(path)))

// PROXY: same interface, controls access; may not create the subject at all
class ImageProxy implements Image {
  private real: Image | null = null
  constructor(private path: string, private user: User) {}
  render() {
    if (!canRead(this.user, this.path)) throw Forbidden()   // protection
    if (this.real == null) this.real = loadFromDisk(this.path) // virtual
    this.real.render()
  }
}

go deeper

for a junior

State the three intents in one sentence each and give the interface test: Adapter changes the interface, Decorator and Proxy keep it.

for a middle

Add the stackability and opt-in distinction between Decorator and Proxy, name the proxy varieties, and give a concrete example of each from real libraries.

for a senior

Discuss trade-offs: broken identity, ordering semantics, debugging depth, when framework interception replaces hand-written wrappers, and how Adapter scales to an anti-corruption layer.

for a principal

Reason about wrappers as a boundary strategy — where translation belongs, which cross-cutting concerns should be proxied by infrastructure (sidecar, gateway, interceptor) rather than in-process, and the organisational cost of deep wrapper stacks.

## Setup A **wrapper** is an object that holds a reference to another object (the *wrappee*) and implements some interface by delegating to it, adding work before, after, or instead of the delegated call. Adapter, Decorator and Proxy are three wrappers with three different reasons to exist. Their code can be nearly identical, so learning them by diagram fails; learn them by the question each answers. ## Adapter — "I cannot change either side" **Problem.** Your client code is written against interface `Target`. A useful component exists but exposes `Adaptee`, an incompatible interface — it is third-party, legacy, generated, or owned by another team. You may not change the client (too many call sites) and you may not change the component (not yours). **Solution.** Write `Adapter implements Target`, hold an `Adaptee`, and translate: rename methods, reorder or repackage parameters, convert units and error models (exceptions ↔ result codes), adapt granularity (one `Target` call fanning out to several `Adaptee` calls). **Key properties.** - The exposed interface differs from the wrapped one. This is the single reliable tell. - Usually one adapter per (Target, Adaptee) pair; adapters are rarely stacked. - Two forms: *object adapter* (composition — the default, works everywhere) and *class adapter* (inherit from both Target and Adaptee — needs multiple inheritance or an interface-plus-class combination; more coupled, can override adaptee behaviour). - Related: **anti-corruption layer** in Domain-Driven Design is Adapter scaled to a subsystem boundary; **ports and adapters / hexagonal architecture** names the whole style after it. ## Decorator — "add a feature to this one object, at runtime" **Problem.** You want optional, combinable behaviours (logging, retry, caching, buffering, compression, encryption, borders on a UI widget) without a subclass for every combination. Subclassing gives you `BufferedCompressedEncryptedStream` and 2^n friends; it also fixes the choice at compile time and applies to the whole class rather than one instance. **Solution.** Decorator implements the *same* interface as the component, holds a component, and enriches the delegated calls. Because input and output types match, decorators nest: `encrypt(compress(buffer(file)))`. **Key properties.** - Same interface in and out — this is what makes stacking possible and type-safe. - Affects a single object, not the class; two instances of the same class can be decorated differently. - Order matters and is a real source of bugs: compress-then-encrypt and encrypt-then-compress give very different results (encrypted data does not compress). - Costs: identity is broken (`decorated != original`, so reference equality, `instanceof`-style checks, hash keys and downcasts to the concrete type all misbehave); stack traces and debugging get deeper; a long chain is hard to reason about. - Everyday sightings: HTTP client middleware, stream/IO libraries, servlet/ASGI/Express middleware chains, resilience wrappers (retry, timeout, circuit breaker). ## Proxy — "same thing, but I control the access path" **Problem.** The client should keep programming against the real object's interface, but access to the real object needs managing: it is expensive to create, lives on another machine, requires authorization, or its lifecycle needs tracking. **Solution.** Proxy implements the same interface as the real subject, holds or can obtain it, and performs access-control work around delegation. **Standard varieties.** - **Virtual proxy** — defers creating/loading the expensive subject until first real use (lazy loading of an image, an ORM lazy association). - **Remote proxy** — local stand-in for an object in another process/machine; handles marshalling, transport, retries. Client stubs in RPC frameworks are exactly this. - **Protection proxy** — checks permissions before forwarding. - **Smart reference / smart proxy** — reference counting, locking, caching, metrics, transaction demarcation. **Key properties.** - Same interface as the subject, like Decorator — but the caller usually does not know a proxy is there and did not choose it, whereas decorators are deliberately composed by whoever builds the object. - Typically one proxy, not a stack, and it often controls the subject's *existence* (creates it, or never creates it), which a decorator never does — a decorator always receives an already-built component. ## The decision procedure 1. Does the exposed interface differ from the wrapped one? → **Adapter**. Stop. 2. Same interface. Does the wrapper manage *whether/where/if-allowed* you reach the real object (lazy creation, network, permissions, lifecycle)? → **Proxy**. 3. Same interface, real object already exists and is always reached, wrapper adds an optional capability the composer opted into and may stack with others? → **Decorator**. 4. Neither, and you actually invented a new small interface over several objects? → that is a **Facade**, not a wrapper of one object. ## Honest caveats - The categories blur. A caching layer is a smart-reference Proxy by intent but a Decorator in mechanics if the caller chooses to add it. Communicate intent ("cache proxy") rather than litigating taxonomy. - Dynamic proxies, interceptors and aspect-oriented weaving (annotations that add transactions, security or metrics) are the same three intents implemented by the framework instead of by hand. - All three add indirection: more allocations, more stack frames, broken object identity, harder debugging. Wrap when the alternative is worse, not reflexively.

  • You have a chain of five decorators around a stream and a bug appears only in production. What makes debugging harder than a single class would be?
    The call passes through five frames per operation, so stack traces are deep and repetitive; behaviour depends on composition order that lives at the construction site, not in any one class; object identity is broken so `==`/`instanceof` checks and logging of the concrete type mislead; and no single file describes the effective behaviour.
  • Is a mock or stub in a unit test an Adapter, a Decorator or a Proxy?
    None of them by intent — a test double replaces the collaborator rather than wrapping it. A *spy* that wraps the real object and records calls before delegating is genuinely a Proxy (smart reference).
  • How do dynamic proxies or annotation-driven interception relate to these patterns?
    They are the same intents implemented generically by a framework: a runtime-generated class implements the target interface and routes calls through interceptors that add transactions, security or metrics. Intent stays Proxy (or Decorator when the caller opts in); only the boilerplate disappears.

A travel plug adapter lets a device meet a foreign socket (Adapter). A surge protector you choose to add keeps the same socket shape and can be daisy-chained with an extension lead (Decorator). A keycard-controlled socket in a lab that only powers up for authorised staff, and only when something is plugged in, is the gatekeeper (Proxy).

saying these in an interview costs you the question

  • 'Decorator and Proxy are the same thing' — they share structure but differ in who chooses them, whether they stack, and whether they control the subject's existence.
  • Calling any wrapper an Adapter — if the exposed interface equals the wrapped one, it is not an Adapter.
  • Claiming Adapter adds new behaviour; it translates, it should not add business logic.
  • Forgetting that decorator order is semantically significant.
  • Ignoring broken identity/equality when objects are wrapped.
  • Saying a Proxy always talks to a remote object; virtual, protection and smart-reference proxies are entirely local.

context