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.
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:
- Does the caller see a different interface from the object inside?
- Is behaviour being added to each call?
- 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:
gateway.pay(500)- checkoutcalls pay(500), in rupees, expecting true or false
- PaytmAdapterturns 500 rupees into { paise: 50000 }
- Paytm SDKcreateCharge({ paise: 50000 }) returns { status: "succeeded" }
- PaytmAdapterturns "succeeded" into true
- 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
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:
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)) // trueThe 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:
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 sentEvery 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).
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) // 1Three 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.
| Decorator | Proxy | |
|---|---|---|
| Its purpose | Add a feature to every call | Control access to the real object |
| Does the real call always happen | Yes, every layer calls the next | Not necessarily: cached, refused, deferred |
| Who builds it | Usually the client, on purpose, often stacked | Usually set up for the client, who should not notice |
| Typical examples | Logging, metrics, retries, compression | Caching, 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.
@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:
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 dosaThe 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 sees | Adds behaviour | Controls access | Wraps | |
|---|---|---|---|---|
| Adapter | Different from the inside | No | No | One object |
| Decorator | Same | Yes, stackable | No | One object, layer on layer |
| Proxy | Same | Only around access | Yes | One object |
| Facade | New and smaller | No, it sequences | No | Many 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.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Adapter | A 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. |
| Decorator | Features 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. |
| Proxy | Caching, 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. |
| Facade | One 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. |
createCharge(req: { paise: number })calls = 0private cache = new Map<string, string[]>()constructor(private kitchen = new Kitchen())const gateway: PaymentGateway = new PaytmAdapter(...)In the interview#
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?
- 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.
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.