Design Patterns & LLD

Structural Patterns: Adapter, Decorator, Facade, Proxy and Composite

The intent of each pattern, how to tell them apart, and the JavaScript Proxy object.

Lesson 0218 min read#structural-patterns#decorator#proxy

On this page

  1. Words used in this lesson
  2. The five at a glance
  3. Adapter: fix the shape
  4. Decorator: add behaviour by wrapping
  5. Facade: one simple door
  6. Proxy: a stand-in with a guard
  7. Composite: one and many look the same
  8. Two more you may meet
  9. Choosing quickly
  10. Interview questions
  11. Key takeaways
  12. What's next

In one line: structural patterns are about how objects are joined together. An adapter makes a wrong-shaped object fit, a decorator adds behaviour by wrapping, a facade hides a messy group of objects behind one simple door, a proxy stands in for another object to control access, and a composite lets one thing and a group of things be used the same way.

Lesson 1 was about creating objects. This lesson is about connecting them. These five patterns look alike on the page, because each one holds another object. They differ in intent, and the interviewer will usually ask you to tell them apart. So for each pattern, remember the intent first and the code second.

Lesson 0 (SOLID and the routine) explains the interface and composition ideas used here. Lesson 1 (creational patterns) explains dependency injection, which we use to pass the inner object in.

What I ran / checked. I ran every TypeScript block with Node 22.22.0 (node --experimental-transform-types; top-level await works because the files are ES modules) and pasted the real output. All files also pass tsc --strict (TypeScript 7.0.2). Pattern names and intents are general knowledge of the classic 1994 catalogue; I quote no text. I could not open one popular pattern guide on GitHub from this sandbox. The Proxy object behaviour was run, not only read. Bridge and Flyweight are only named in a table, with no code.

Words used in this lesson

  • A structural pattern is a pattern about how classes and objects are combined into larger structures.
  • A wrapper is an object that holds another object and forwards calls to it. Adapter, decorator and proxy are all wrappers.
  • The target interface is the interface the caller wants to use. The adaptee is the existing object with the wrong shape.
  • A legacy API is an old or third-party interface you cannot or should not change.
  • A cross-cutting concern is a need that cuts across many classes: logging, caching, retries, timing, permissions.
  • A subsystem is a group of classes that work together for one job (inventory, payments, shipping).
  • A leaf is an object with no children. A node that has children is a composite.
  • A tree is a structure where each item has one parent and may have many children.
  • Lazy means "do it only when it is first needed".
  • A trap (in JavaScript Proxy) is a function that intercepts an operation such as get or set. (Not the same as an interview trap.)

The five at a glance

PatternIntent in one lineInterface of the wrapperTypical sign
AdapterMake an existing thing fit the interface I wantDifferent from the inner one"Their API is shaped wrong"
DecoratorAdd behaviour without changing the classSame as the inner one"Add logging / caching / retry"
FacadeOne simple entry to a complex groupNew, simpler interface"Callers must know ten steps"
ProxyControl access to the real objectSame as the inner one"Lazy load / permissions / remote"
CompositeTreat one and many the sameSame for leaf and group"Tree of parts"

Adapter changes the interface. Decorator and proxy keep it. Facade makes a new, smaller one. The trick: decorator adds behaviour, proxy controls access, and both look the same in code. Say the intent out loud.

Adapter: fix the shape

You have an old library with callbacks and Fahrenheit. Your app wants promises and Celsius. You cannot edit the library. Write a small class that speaks your interface on one side and theirs on the other.

JavaScript
// Third-party / legacy code we cannot change: callbacks, odd names, Fahrenheit.class LegacyWeather {  fetchTempF(city: string, cb: (err: Error | null, f?: number) => void) {    setTimeout(() => (city === "nowhere" ? cb(new Error("unknown city")) : cb(null, 68)), 1);  }}// The interface our app wants: promises, Celsius.interface WeatherService { celsius(city: string): Promise<number>; }class LegacyWeatherAdapter implements WeatherService {  constructor(private legacy: LegacyWeather) {}  celsius(city: string): Promise<number> {    return new Promise((resolve, reject) => {      this.legacy.fetchTempF(city, (err, f) => (err ? reject(err) : resolve(((f! - 32) * 5) / 9)));    });  }}const service: WeatherService = new LegacyWeatherAdapter(new LegacyWeather());console.log(await service.celsius("Paris"));await service.celsius("nowhere").catch(e => console.log("error:", e.message));

Output:

TEXT
20error: unknown city

LegacyWeatherAdapter implements WeatherService, which is what the app knows. Inside, it calls the old callback method, turns it into a promise, and converts 68 F to 20 C. If the old library changes, only the adapter changes. If you switch libraries, you write a new adapter and nothing else moves. That is the dependency inversion rule from Lesson 0 in action.

Adapter or a plain function? For one conversion, a function celsiusFrom(legacy) is enough. Use a class when you want to implement a shared interface, keep state, or have more than one method to adapt.

Decorator: add behaviour by wrapping

Logging, caching and retries are needed in many places. Putting them inside each class spreads one idea over many files and breaks the single responsibility rule. A decorator wraps an object that has the same interface and adds one thing before or after the call.

JavaScript
interface Fetcher { get(key: string): Promise<string>; }let calls = 0;const base: Fetcher = {                       // fails on its first two calls  async get(key) {    calls++;    if (calls <= 2) throw new Error("timeout");    return "value-of-" + key;  },};class WithRetry implements Fetcher {  constructor(private inner: Fetcher, private times: number) {}  async get(key: string): Promise<string> {    for (let i = 1; ; i++) {      try { return await this.inner.get(key); }      catch (e) { if (i >= this.times) throw e; console.log(`  retry ${i}`); }    }  }}class WithCache implements Fetcher {  private seen = new Map<string, string>();  constructor(private inner: Fetcher) {}  async get(key: string): Promise<string> {    if (this.seen.has(key)) return this.seen.get(key)!;    const v = await this.inner.get(key);    this.seen.set(key, v);    return v;  }}class WithLog implements Fetcher {  constructor(private inner: Fetcher, private name: string) {}  async get(key: string): Promise<string> {    console.log(`${this.name}: get ${key}`);    return this.inner.get(key);  }}// Wrap from the inside out. Same interface at every layer.const fetcher: Fetcher = new WithLog(new WithCache(new WithRetry(base, 3)), "app");console.log(await fetcher.get("a"));console.log(await fetcher.get("a"));console.log("base calls:", calls);

Output:

TEXT
app: get a  retry 1  retry 2value-of-aapp: get avalue-of-abase calls: 3

Read the output from the top. The call goes WithLog first, then WithCache (miss), then WithRetry, then the base. The base fails twice, WithRetry tries again twice (retry 1, retry 2), and the third call works. The second get("a") stops at WithCache and never reaches the base: base calls: 3, not 4. Because all layers share the Fetcher interface, you can stack them in any order, and the order matters. Put WithCache outside WithRetry, as here, to cache the final answer. Put it inside, and each retry attempt would consult the cache.

Two notes. First, "decorator" the pattern is not the same as the TypeScript or JavaScript @decorator syntax, although that syntax can be used to build it. Second, higher-order functions are decorators too: withRetry(fn) returns a new function with the same signature. Express middleware (Lesson 4) is a close cousin.

Facade: one simple door

A caller who wants to "place an order" should not have to know that you reserve stock, charge the card, schedule the shipment, and undo the reservation if payment fails. A facade is a class with a small, friendly API that hides that sequence.

JavaScript
// Three subsystems, each with its own small API.class Inventory {  private stock = new Map([["pen", 2]]);  reserve(sku: string, qty: number) {    const have = this.stock.get(sku) ?? 0;    if (have < qty) throw new Error("out of stock: " + sku);    this.stock.set(sku, have - qty);  }  release(sku: string, qty: number) { this.stock.set(sku, (this.stock.get(sku) ?? 0) + qty); }}class Payments { charge(amount: number) { if (amount > 100) throw new Error("card declined"); return "pay-1"; } }class Shipping { schedule(sku: string) { return "ship-" + sku; } }// Facade: one simple method for callers, the order of steps is hidden here.class OrderFacade {  constructor(private inv: Inventory, private pay: Payments, private ship: Shipping) {}  placeOrder(sku: string, qty: number, amount: number): string {    this.inv.reserve(sku, qty);    try {      const payId = this.pay.charge(amount);      return `${payId}/${this.ship.schedule(sku)}`;    } catch (e) {      this.inv.release(sku, qty);               // undo the reservation      throw e;    }  }}const shop = new OrderFacade(new Inventory(), new Payments(), new Shipping());console.log(shop.placeOrder("pen", 1, 5));for (const [qty, amount] of [[5, 5], [1, 500], [1, 5]]) {  try { console.log(shop.placeOrder("pen", qty, amount)); }  catch (e) { console.log("failed:", (e as Error).message); }}

Output:

TEXT
pay-1/ship-penfailed: out of stock: penfailed: card declinedpay-1/ship-pen

The first order works. The second asks for 5 pens when there are fewer, so it fails at the first step. The third fails at payment, and the facade releases the reservation. The proof is the fourth order: it succeeds, which would not happen if the stock had stayed reserved. The caller wrote one line each time and knew nothing about the three subsystems.

Facade is a convenience, not a wall. The subsystems are still available to code that needs more control. Also watch for a facade that grows into a god object that does everything (an anti-pattern, see Lesson 4). A facade should only coordinate; the real work stays in the subsystems.

Proxy: a stand-in with a guard

A proxy has the same interface as the real object and controls access to it. The classic kinds:

KindWhat it doesExample
Virtual (lazy)Creates the real object only when first neededLoad a big report on first read
ProtectionChecks permission firstOnly some users may read
RemoteHides that the real object is on another machineAn RPC client stub
Caching / loggingRemembers results or records callsA cache in front of a service
JavaScript
// 1. The classic Proxy: same interface, controls access to the real object.interface Report { read(user: string): string; }class RealReport implements Report {  constructor() { console.log("(expensive load)"); }  read() { return "secret numbers"; }}class ReportProxy implements Report {          // lazy + protection proxy  private real?: RealReport;  constructor(private allowed: string[]) {}  read(user: string) {    if (!this.allowed.includes(user)) return "denied";    this.real ??= new RealReport();            // created only on first allowed use    return this.real.read();  }}const proxy = new ReportProxy(["ann"]);console.log(proxy.read("bob"));console.log(proxy.read("ann"));console.log(proxy.read("ann"));// 2. JavaScript's built-in Proxy object: intercept get and set on any object.const log: string[] = [];const user = new Proxy({ name: "Ann", age: 30 } as Record<string, unknown>, {  get(target, prop, receiver) { log.push("get " + String(prop)); return Reflect.get(target, prop, receiver); },  set(target, prop, value, receiver) {    if (prop === "age" && (typeof value !== "number" || value < 0)) throw new TypeError("age must be >= 0");    return Reflect.set(target, prop, value, receiver);  },});user.age = 31;try { user.age = -5; } catch (e) { console.log((e as Error).message); }console.log(user.name, user.age, log);

Output:

TEXT
denied(expensive load)secret numberssecret numbersage must be >= 0Ann 31 [ 'get name', 'get age' ]

The first half is the classic pattern. bob is refused and the expensive object is never built (no (expensive load) yet). ann triggers the load once, and the second read reuses it. The second half uses the language's own Proxy object. It intercepts get and set on a plain object: the set trap refused -5, and the get trap recorded the two reads. Use Reflect.get and Reflect.set inside the traps to keep the default behaviour for everything else.

Where the language Proxy object shows up in the real world: change tracking, validation, and "reactive" data in front-end libraries (I only know this as general knowledge; I did not open any library for this lesson). Its cost is that traps run on every access, so do not put one on a hot path without measuring.

Decorator versus proxy. The code can look the same. The decorator is chosen by the caller, to add a feature (retry, log). The proxy is usually in charge of the life or access of the real object: it may decide to never create it at all, as bob showed.

Composite: one and many look the same

A folder holds files and other folders. A menu holds items and sub-menus. A shape group holds shapes and other groups. If the caller must check "is this a file or a folder?" everywhere, the code fills with ifs. With a composite, leaf and group share one interface, and the group simply passes the call on to its children.

JavaScript
// Composite: files and folders share one interface, so callers treat them alike.interface FsNode { name: string; size(): number; print(indent?: string): string[]; }class File implements FsNode {  constructor(readonly name: string, private bytes: number) {}  size() { return this.bytes; }  print(indent = "") { return [`${indent}${this.name} (${this.bytes})`]; }}class Folder implements FsNode {  private children: FsNode[] = [];  constructor(readonly name: string) {}  add(...n: FsNode[]) { this.children.push(...n); return this; }  size(): number { return this.children.reduce((sum, c) => sum + c.size(), 0); }  print(indent = ""): string[] {    return [`${indent}${this.name}/ (${this.size()})`, ...this.children.flatMap(c => c.print(indent + "  "))];  }}const root = new Folder("src").add(  new File("index.ts", 120),  new Folder("lib").add(new File("a.ts", 300), new File("b.ts", 80), new Folder("empty")),);console.log(root.print().join("\n"));console.log("total:", root.size());

Output:

TEXT
src/ (500)  index.ts (120)  lib/ (380)    a.ts (300)    b.ts (80)    empty/ (0)total: 500

size() on src adds up the sizes of all children, even the nested folder, and empty has size 0. print() calls itself on each child with more indent. The caller code (root.size()) is the same for one file or the whole tree. The recursion is the point: a composite is a tree, and tree questions (size, search, print) become a method on the node.

Trade-off: the shared interface may carry methods that make no sense for a leaf (add on a file). Here I kept add on Folder only, so a File cannot be asked to add children. That is safer, but then a caller holding a FsNode cannot add without a type check. Say this choice out loud in an interview.

Two more you may meet

PatternOne lineWhen it comes up
BridgeSplit a class into two parts (what it does and how) that change on their own, joined by compositionShapes with many renderers; notifications with many channels
FlyweightShare the common part of many small objects to save memoryCharacters in a text editor; map tiles

I do not show code for these because they are rare in interviews, and I have not run an example for them here.

Choosing quickly

Your needPattern
"Their interface is wrong for us"Adapter
"I want logging, caching, retry, timing around this"Decorator
"Callers do five calls in a fixed order"Facade
"Create it late, or check permission first"Proxy
"It is a tree: part and whole"Composite

Interview questions

What is the difference between Adapter and Decorator? (Must know)

An adapter changes the interface. It lets code written for interface A use an object that has interface B. A decorator keeps the interface and adds behaviour. I say it this way: adapter is about shape, decorator is about extra behaviour. In my example the adapter turned callbacks and Fahrenheit into promises and Celsius, and the decorators added retry, cache and logging to a Fetcher without changing its shape.

What is the difference between Decorator and Proxy? (Senior signal)

Both keep the same interface and hold the inner object. The intent differs. A decorator adds a feature chosen by the caller, and the inner object always exists. A proxy controls access to the real object, so it may delay creating it, refuse the call, or hide that the object is remote. If the wrapper decides whether the real object is even touched, it is a proxy.

When would you use a Facade? (Must know)

When callers need a task done and today they must know a fixed sequence of calls across several classes. The facade holds that sequence, including the clean-up on failure (releasing stock when payment fails). It also gives one place to change if the subsystems change. I keep the subsystems public for advanced users and I keep the facade thin, so it does not become a god object.

Follow-up: "Is a facade the same as an API gateway?" The idea is similar (one simple front for many parts), but a gateway is a network service, and a facade is a class in one program.

How would you add logging and caching to a service without editing it? (Must know)

I define an interface for the service, then write a WithLog and a WithCache class that implement the same interface and wrap an inner one. I compose them in the composition root: new WithLog(new WithCache(real)). Order matters, so I choose it on purpose and explain it. This keeps each class to one job.

Does the order of stacked decorators matter? (Senior signal)

Yes. In my example, WithCache outside WithRetry caches the final successful value. With WithCache inside, every retry would check the cache first, which is wasted work. Logging outside everything logs each logical call, and logging inside logs every attempt. I pick the order from the question "at which level do I want this effect?".

Give an example of Composite. (Must know)

A file system. File and Folder both implement size() and print(). A folder passes the call to each child and adds up the results. The caller does not check types. Other examples: a menu with sub-menus, a UI component tree, an organisation chart, or a group of shapes in a drawing tool.

What is a virtual proxy? (Must know)

A proxy that creates the expensive real object only on first use. In my example the real report was built only when an allowed user first read it. Typical uses are big files, images and database-backed objects. The caller sees the same interface and does not know the object may not exist yet.

What is the JavaScript Proxy object, and how does it relate to the pattern? (Senior signal)

It is a built-in object that wraps a target and lets you define traps (get, set, has, deleteProperty and more). It is the language's own tool for building a proxy. I used a set trap to refuse a negative age and a get trap to record reads. The cost is that every access runs your code, so I measure before putting it on a hot path. Also Proxy works on plain objects with no interface at all, which the classic pattern does not.

Where would you use an Adapter in a real project? (Must know)

Wherever an outside library or old service has a shape I do not want in my business code: a payment SDK with its own types, an old callback API, a vendor's response format. I define my own small interface first (PaymentGateway) and write one adapter per vendor. Business code never imports the vendor. A vendor change touches one file.

A composite interface has add() on every node. What is the problem? (Trap)

A leaf like File cannot add children, so either add throws on a leaf (which breaks the Liskov rule from Lesson 0) or it does nothing silently. The safer design puts add only on the composite, at the cost of a type check when a caller holds the general node type. I choose the safe version and say the trade-off.

How do these patterns relate to the SOLID principles? (Senior signal)

Adapter and facade support dependency inversion: business code depends on my interface, not a vendor's. Decorator supports open/closed: new behaviour is a new wrapper, not an edit. Decorator also supports single responsibility, because retry, cache and log are separate classes. Composite supports Liskov substitution if leaf and group truly keep the same promise.

When would you not use any of these? (Trap)

When there is one call site and the "wrapper" only forwards calls. A class that wraps another and adds nothing is noise. I also avoid stacking four decorators when a three-line try/catch would do. I add the pattern when the second or third use appears, or when a vendor boundary makes the change cheap later.

Key takeaways

  • Structural patterns join objects. Adapter, decorator and proxy are wrappers; they differ in intent.
  • Adapter changes the interface. Decorator keeps it and adds a feature. Proxy keeps it and controls access. Facade makes a new, smaller one. Composite gives leaf and group the same interface.
  • Stacked decorators work because they share one interface. Their order changes the result.
  • A facade coordinates; it should not become a god object.
  • The JavaScript Proxy object is a language tool for building proxies, with a cost on every access.
  • Use a pattern when a real change or a vendor boundary asks for it, not before.

What's next

Next: Behavioural patterns: Strategy, Observer, Command, State and Chain of Responsibility, Lesson 3. Practise: wrap a function you own with a withTiming decorator that prints how long it took, then stack it with a withRetry.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series