Library
10 min read

Adapter vs Decorator vs Proxy vs Facade

Published

All four are a class that holds another object and calls it. Read their code without the class names and you often cannot tell which one you are looking at. That is not a failure of understanding. The four patterns are named for why the wrapper is there, not for what the code looks like.

Four everyday pictures, three of them from the LLD course:

  • Adapter is the travel plug. Your charger has Indian pins, the wall in London has different holes. The plug changes the shape. It adds no power.
  • Decorator is the add-ons on a dosa. It is still a dosa, served the same way, now with cheese, and then butter on top of that.
  • Proxy is the clinic receptionist. You came to see the doctor. The receptionist decides if and when you get in, and answers "is my report ready?" without disturbing the doctor at all.
  • Facade is the hotel reception. One desk in front of housekeeping, the kitchen, billing and the cab desk.
Your answer

You wrap a payment gateway in a class with the same pay() method that writes a log line before every call and then passes the call on. Is that a Decorator or a Proxy? What would have to change for it to be the other one?

Three questions#

Ask these about any wrapper and you can name it:

  1. Does the caller see a different interface from the object inside?
  2. Is behaviour being added to each call?
  3. Is access to the real object being controlled: whether, when, or how often it is called?

Then one more, for the facade: how many objects are behind it? Follow a single call through each one:

Follow one call through each wrapper
gateway.pay(500)
  1. checkoutcalls pay(500), in rupees, expecting true or false
  2. PaytmAdapterturns 500 rupees into { paise: 50000 }
  3. Paytm SDKcreateCharge({ paise: 50000 }) returns { status: "succeeded" }
  4. PaytmAdapterturns "succeeded" into true
  5. checkoutgets true, never learns the SDK speaks paise
Interface the caller sees
Changes it
Adds behaviour
No
Controls access to the real one
No
Wraps
One object
The travel plug: same socket in the wall, different pins.

Adapter: change the shape#

The new gateway's SDK wants paise, names its method createCharge and replies with an object. Checkout already expects pay(rupees) returning a boolean. The adapter translates, and does nothing else:

ts
interface PaymentGateway {
  pay(rupees: number): boolean
}
 
// The new vendor's SDK: paise, a different name, a different reply.
class PaytmSdk {
  createCharge(req: { paise: number }) {
    return { status: 'succeeded', paise: req.paise }
  }
}
 
class PaytmAdapter implements PaymentGateway {
  constructor(private sdk: PaytmSdk) {}
  pay(rupees: number) {
    const res = this.sdk.createCharge({ paise: rupees * 100 })
    return res.status === 'succeeded'
  }
}
 
const gateway: PaymentGateway = new PaytmAdapter(new PaytmSdk())
console.log(gateway.pay(500)) // true

The interface going in (createCharge) is different from the interface coming out (pay). That is the whole test for an adapter.

Decorator: same shape, more behaviour#

A decorator has the same interface as the thing it wraps, so it can wrap another decorator, and you can stack them in any order:

ts
interface PaymentGateway {
  pay(rupees: number): boolean
}
 
class Razorpay implements PaymentGateway {
  pay(rupees: number) {
    console.log(`razorpay charged ${rupees}`)
    return true
  }
}
 
class WithLogging implements PaymentGateway {
  constructor(private inner: PaymentGateway) {}
  pay(rupees: number) {
    console.log(`pay ${rupees} started`)
    return this.inner.pay(rupees)
  }
}
 
class WithReceipt implements PaymentGateway {
  constructor(private inner: PaymentGateway) {}
  pay(rupees: number) {
    const ok = this.inner.pay(rupees)
    if (ok) console.log(`receipt for ${rupees} sent`)
    return ok
  }
}
 
const gateway = new WithLogging(new WithReceipt(new Razorpay()))
gateway.pay(500)
// > pay 500 started
// > razorpay charged 500
// > receipt for 500 sent

Every layer always calls the one inside it. A decorator adds something to the call. It does not decide whether the call happens.

Proxy: same shape, controls access#

A proxy also has the same interface, but its job is to stand in front of the real object and decide whether, when and how often the real one is used. The common kinds:

  • Caching proxy: answers repeated questions itself.
  • Lazy proxy: does not create the expensive real object until the first call.
  • Protection proxy: checks permissions and refuses some calls.
  • Remote proxy: looks like a local object, actually sends each call over the network (a gRPC client stub is one).
ts
interface MenuSource {
  menu(restaurantId: string): string[]
}
 
class MenuApi implements MenuSource {
  calls = 0
  menu(restaurantId: string) {
    this.calls += 1 // a network call in real life
    return ['Masala dosa', 'Filter coffee']
  }
}
 
class CachedMenu implements MenuSource {
  private cache = new Map<string, string[]>()
  constructor(private real: MenuSource) {}
  menu(restaurantId: string) {
    const hit = this.cache.get(restaurantId)
    if (hit) return hit
    const fresh = this.real.menu(restaurantId)
    this.cache.set(restaurantId, fresh)
    return fresh
  }
}
 
const api = new MenuApi()
const menus: MenuSource = new CachedMenu(api)
menus.menu('udupi-42')
menus.menu('udupi-42')
menus.menu('udupi-42')
console.log(api.calls) // 1

Three calls went in, one reached the real API. The caller holds a MenuSource and has no idea a cache is there.

Decorator vs Proxy: the hardest pair#

Put WithLogging and CachedMenu side by side and the structure is identical: same interface, holds the real object, forwards the call. Three things separate them.

DecoratorProxy
Its purposeAdd a feature to every callControl access to the real object
Does the real call always happenYes, every layer calls the nextNot necessarily: cached, refused, deferred
Who builds itUsually the client, on purpose, often stackedUsually set up for the client, who should not notice
Typical examplesLogging, metrics, retries, compressionCaching, lazy loading, permission checks, remote stubs

So the logging wrapper in the question at the top is a decorator: it adds a line and always calls through. Make it skip the call for amounts over a limit, or answer from a cache, and it becomes a proxy.

Caching is the famous borderline case. The original design patterns book lists caching as a job for a proxy, and that is the line this page draws. The decorator chapter in the LLD course wraps a price service in a cache and calls it a decorator, because the interface stays the same and a feature is added, and plenty of codebases name it that way too. Both readings are defensible. Retries sit on the same line. An interviewer cares much more that you can say what the wrapper is for, and why you picked the name, than that you pick the "right" label.

Two TypeScript features with the same names

@log above a method is a TypeScript decorator, a language feature (standard decorators arrived in TypeScript 5.0). It wraps a class or method when the class is defined. The Decorator pattern wraps one object at runtime, and two orders can have different stacks. They are related in spirit and different in mechanism.

JavaScript's built in Proxy (new Proxy(target, handler)) intercepts property reads and writes on any object. It is a tool you could build either pattern with. Vue 3's reactivity system is built on it.

Facade: one simple door in front of many#

The facade is the odd one out. It does not wrap one object with the same interface. It puts a new, smaller interface in front of several objects, and gets the sequence right once:

ts
class Inventory {
  reserve(item: string) { console.log(`reserved ${item}`) }
}
class Payments {
  charge(rupees: number) { console.log(`charged ${rupees}`) }
}
class Kitchen {
  send(item: string) { console.log(`kitchen: ${item}`) }
}
class Riders {
  assign(item: string) { console.log(`rider for ${item}`) }
}
 
class OrderFacade {
  constructor(
    private inventory = new Inventory(),
    private payments = new Payments(),
    private kitchen = new Kitchen(),
    private riders = new Riders(),
  ) {}
  place(item: string, rupees: number) {
    this.inventory.reserve(item) // always first
    this.payments.charge(rupees)
    this.kitchen.send(item)
    this.riders.assign(item)
  }
}
 
new OrderFacade().place('Masala dosa', 120)
// > reserved Masala dosa
// > charged 120
// > kitchen: Masala dosa
// > rider for Masala dosa

The facade chapter starts with the bug this prevents: one team charged first and reserved second, and customers paid for dishes that had just sold out.

Adapter vs Facade is the other pair that gets mixed up, because both change the interface. An adapter makes one object fit an interface the caller already has. A facade invents a new, simpler interface over many objects. The adapter is about fitting, the facade is about hiding.

All four in one table#

Interface the caller seesAdds behaviourControls accessWraps
AdapterDifferent from the insideNoNoOne object
DecoratorSameYes, stackableNoOne object, layer on layer
ProxySameOnly around accessYesOne object
FacadeNew and smallerNo, it sequencesNoMany objects

The same ideas show up one level up, between machines. A reverse proxy, a load balancer and an API gateway are proxies and facades for whole services. That is in forward proxy vs reverse proxy vs API gateway.

ChoiceWhat you gainWhat you payPick it when
AdapterA vendor or legacy API fits your interface, and the change lives in one class.Leaky when the two sides really differ (sync vs async, different error models).Switching vendors, wrapping a legacy SDK, keeping a third party type out of your code.
DecoratorFeatures added and combined per object without a class for every combination.Long stacks are hard to debug, and the order of layers matters.Cross cutting additions: logging, metrics, retries, add-ons that combine freely.
ProxyCaching, laziness or permission checks without the caller changing.Hidden behaviour: stale caches and surprise refusals that the caller cannot see.The real object is slow, expensive, remote, or needs guarding.
FacadeOne correct entry point, the sequence written once.Can grow into a god object that every team adds a method to.Callers keep using the same several classes in the same order.
TypeScript you just picked up
createCharge(req: { paise: number })
An inline object type: the argument must be an object with a number field called paise.
calls = 0
A class field with a starting value. Its type, number, is inferred.
private cache = new Map<string, string[]>()
A private Map from strings to arrays of strings.
constructor(private kitchen = new Kitchen())
A parameter property with a default: if nothing is passed, a new Kitchen is used.
const gateway: PaymentGateway = new PaytmAdapter(...)
Typing the variable as the interface, so the rest of the code cannot reach the adapter's internals.

In the interview#

Interviewer
Decorator and Proxy have the same structure. How do you tell them apart?
You
By intent. A decorator adds behaviour to every call and always passes it through, and you stack them on purpose, like logging around retries around the real gateway. A proxy controls access to the real object: it may answer from a cache, create the real object lazily, check permissions, or send the call over the network, and the caller usually does not know it is there. So if the wrapper can decide that the real object is not called, I call it a proxy.
Gives a test you can apply to any wrapper: can it skip the real call?
Interviewer
And an adapter?
You
An adapter changes the interface. The other two keep it the same. I use one when a vendor SDK does not match the interface my code already depends on, so the translation lives in one class instead of forty call sites. A facade also changes the interface, but it puts one new, simpler interface over several classes.
Which wrapper?
A class that turns a SOAP client's XML responses into the JSON shaped objects your code uses
An image object that only downloads the full file the first time display() is called
Wrapping an HTTP client so every request gets a trace id header, then wrapping that so every request is timed
A checkout() method that calls cart, coupons, payments and notifications in the right order

Checkpoint#

Checkpoint

1. Which question separates an adapter from a decorator and a proxy?

2. A wrapper with the same interface answers repeated calls from memory and only reaches the real API on a miss. What is it?

3. How is a facade different from an adapter?

Worth memorising
  • Adapter changes the shape. The travel plug.
  • Decorator keeps the shape and adds behaviour, and stacks. Add-ons on a dosa.
  • Proxy keeps the shape and controls access. The clinic receptionist.
  • Facade puts one simple door in front of many objects. The hotel reception.
  • Decorator or proxy? Ask whether the wrapper can skip the real call.
Say this in 60 seconds

Adapter, Decorator, Proxy and Facade all hold another object and call it, so I tell them apart by intent with three questions: does the caller see a different interface, is behaviour added, and is access to the real object controlled. An adapter changes the interface and adds nothing, like turning a vendor SDK that works in paise into the pay in rupees method my code expects. A decorator keeps the interface and adds behaviour such as logging or retries, always calls through, and stacks. A proxy keeps the interface and controls access: caching, lazy creation, permission checks or a remote stub, and the caller usually does not know it is there. The test between those two is whether the wrapper can skip the real call. A facade is different again: a new, simpler interface over several objects, which gets a sequence like reserve, charge, cook and assign right in one place.