All chapters in this track
- 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
- 02Chapter 2: Patterns and SOLID: How They Relate
- 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
- 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
- 05Chapter 5: Structural Patterns I: Adapter & Facade
- 06Chapter 6: Structural Patterns II: Decorator & Proxy
- 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
- 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
- 09Chapter 9: Behavioral Patterns II: Observer & Mediator
- 10Chapter 10: Behavioral Patterns III: Command & Memento
- 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
- 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
- 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
- 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
- 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
- 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
- 17Chapter 17: Architectural Patterns III: The Repository Pattern
- 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt
Chapter 6 of 18
Chapter 6: Structural Patterns II: Decorator & Proxy
Why this matters
Decorator and Proxy share almost the exact same structural shape in code — a class implementing the same interface as the object it wraps, holding a reference to that object — which is precisely why they're worth studying together: the code looks nearly identical, but the intent is completely different, and that intent is what should drive which one you reach for.
Decorator
The problem it solves: you need to add behavior to an individual object dynamically, without affecting other instances of the same class, and without creating a combinatorial explosion of subclasses for every combination of behaviors.
Before — subclassing for every combination, which explodes combinatorially:
class Coffee { cost(): number { return 200; } }
class CoffeeWithMilk extends Coffee { cost(): number { return super.cost() + 50; } }
class CoffeeWithSugar extends Coffee { cost(): number { return super.cost() + 25; } }
class CoffeeWithMilkAndSugar extends Coffee { cost(): number { return super.cost() + 75; } }
// Adding "whip" as another option means 4 more subclasses to cover every combination.
After — Decorator composes behaviors at runtime, no combinatorial explosion:
interface Beverage {
cost(): number;
description(): string;
}
class Coffee implements Beverage {
cost(): number { return 200; }
description(): string { return "Coffee"; }
}
abstract class BeverageDecorator implements Beverage {
constructor(protected wrapped: Beverage) {}
abstract cost(): number;
abstract description(): string;
}
class MilkDecorator extends BeverageDecorator {
cost(): number { return this.wrapped.cost() + 50; }
description(): string { return `${this.wrapped.description()} + Milk`; }
}
class SugarDecorator extends BeverageDecorator {
cost(): number { return this.wrapped.cost() + 25; }
description(): string { return `${this.wrapped.description()} + Sugar`; }
}
// Any combination, composed at runtime, with no new classes needed per combination:
const order = new SugarDecorator(new MilkDecorator(new Coffee()));
console.log(order.description(), order.cost()); // "Coffee + Milk + Sugar" 275
class Beverage:
def cost(self) -> int: return 200
def description(self) -> str: return "Coffee"
class BeverageDecorator(Beverage):
def __init__(self, wrapped: Beverage) -> None:
self._wrapped = wrapped
class MilkDecorator(BeverageDecorator):
def cost(self) -> int: return self._wrapped.cost() + 50
def description(self) -> str: return f"{self._wrapped.description()} + Milk"
order = MilkDecorator(Beverage())
When to use it: you need to add optional, combinable behavior to individual objects at runtime, and the number of combinations would otherwise require a subclass per combination.
When to avoid it: if there's only one or two fixed combinations that never change, plain subclassing or even a simple boolean flag is simpler — Decorator earns its place specifically when combinability is the actual requirement.
Complexity cost: each decorator adds a layer of indirection; deeply stacked decorators can make debugging harder (a stack trace through five decorators is less immediately readable than one flat method) — a real cost worth weighing against the combinatorial alternative it avoids.
Proxy
The problem it solves: you need to control access to an object — adding a check, deferring expensive creation, or crossing a boundary (network, process) — without the calling code needing to know that control is happening.
interface Image {
display(): void;
}
class RealImage implements Image {
constructor(private filename: string) {
this.loadFromDisk(); // expensive — happens immediately on construction
}
private loadFromDisk(): void { console.log(`Loading ${this.filename} from disk`); }
display(): void { console.log(`Displaying ${this.filename}`); }
}
// Proxy defers the expensive load until display() is actually called (lazy loading):
class ImageProxy implements Image {
private realImage: RealImage | null = null;
constructor(private filename: string) {}
display(): void {
if (!this.realImage) {
this.realImage = new RealImage(this.filename); // only loads when actually needed
}
this.realImage.display();
}
}
const image = new ImageProxy("photo.png"); // no disk load yet
image.display(); // loads and displays, only now
class RealImage:
def __init__(self, filename: str) -> None:
self.filename = filename
self._load_from_disk()
def _load_from_disk(self) -> None:
print(f"Loading {self.filename} from disk")
def display(self) -> None:
print(f"Displaying {self.filename}")
class ImageProxy:
def __init__(self, filename: str) -> None:
self._filename = filename
self._real_image: RealImage | None = None
def display(self) -> None:
if self._real_image is None:
self._real_image = RealImage(self._filename)
self._real_image.display()
Common Proxy variants, all sharing this same shape with different intents:
- Virtual Proxy — defers expensive object creation until actually needed (the example above).
- Protection Proxy — adds an access-control check before delegating (e.g., verifying permissions before allowing an operation).
- Remote Proxy — represents an object that actually lives in a different process or machine, hiding the network call behind a normal-looking method call.
- Caching Proxy — returns a cached result instead of delegating, when appropriate.
When to use it: you need to interpose control (lazy loading, access checks, caching, remote access) transparently, without the calling code changing at all.
When to avoid it: if the control logic is simple and there's no real need to hide it from the caller, adding a Proxy layer just for structure's sake is unnecessary — a straightforward conditional at the call site may be clearer.
Complexity cost: an extra class matching the wrapped interface — low mechanical cost, but proxies (especially Remote Proxy) can hide genuinely important behavior (a network call, a permission check) behind what looks like an ordinary method call, which is a real trade-off between transparency and discoverability.
Decorator vs. Proxy: same shape, different intent
[SENIOR INSIGHT] Both wrap an object behind the same interface. The difference is entirely about intent, not structure: Decorator adds new responsibility/behavior to the object (the wrapped object still does its job, plus more); Proxy controls access to the object (the wrapped object may not even run at all, or runs differently based on a check). If you're stacking multiple wrappers to combine independent features, you're using Decorator's intent. If you're adding exactly one wrapper to gate, defer, or redirect access, you're using Proxy's intent — even though the code for either could look almost identical.
Common mistakes
- Stacking Proxies the way Decorators are meant to be stacked. Multiple protection/caching/remote proxies chained together for combinability usually signals the intent has drifted into Decorator territory, or that the design needs rethinking.
- Building a Decorator hierarchy for behaviors that never actually combine in practice. If only one or two fixed combinations are ever used, plain subclasses are simpler than the general Decorator machinery.
- Hiding an expensive or risky operation (a network call, a permission-denying check) behind a Proxy with no signal to the caller that something non-trivial is happening — this is a real discoverability cost, and sometimes a method name that hints at it (
fetchRemote()instead ofget()) is a better trade-off than perfect transparency. - Confusing the two patterns' intents in a design discussion, leading to a Proxy being built where combinable behavior (Decorator) was actually needed, or vice versa.
Checklist: choosing between Decorator and Proxy
- Ask: does the wrapped object still do its full job, with something added? → Decorator.
- Ask: does the wrapper control whether or how the wrapped object's job happens at all (deferred, denied, redirected, cached)? → Proxy.
- Check whether behaviors genuinely need to combine — if not, Decorator's flexibility isn't earning its keep, and a simpler subclass may do.
- For Proxy, consider whether hiding the control transparently is actually a good idea here, or whether a more visible method name/signature would serve callers better.
Discussion / interview questions
-
"Decorator and Proxy have nearly identical structure. What actually distinguishes them?" Strong answer: Intent, not structure — Decorator adds behavior to an object that still fully executes; Proxy controls access to (or replaces/defers) the object's execution. Gives an example of each.
-
"When would Decorator be a better choice than a subclass?" Strong answer: When behaviors need to combine in multiple combinations at runtime — subclassing for every combination grows combinatorially, while Decorator composes them without new classes per combination.
-
"What are the different intents a Proxy can serve, and can you name one real example of each?" Strong answer: Virtual (lazy loading — e.g., deferring an expensive image load), Protection (access control — e.g., checking permissions before an operation), Remote (hiding a network call behind a local-looking interface), Caching (returning a cached result). Names at least two correctly with an example.
-
"What's a downside of using a Proxy to hide a network call behind what looks like a normal method call?" Strong answer: Discoverability — a caller reading
service.getUser(id)has no visual signal it might be a slow network call rather than an in-memory lookup, which can lead to accidental performance problems (e.g., calling it in a tight loop) that wouldn't happen if the cost were visible in the API.
Chapter summary / cheat sheet
- Decorator and Proxy share nearly identical code structure — same interface, wraps another instance of it — but differ entirely in intent.
- Decorator: adds behavior, meant to be stacked/combined; avoids a combinatorial explosion of subclasses.
- Proxy: controls access (lazy-load, protect, redirect remotely, cache); typically one wrapper, not stacked for combinability.
- Both carry a real trade-off: Decorator trades subclass explosion for indirection depth; Proxy trades caller convenience for reduced discoverability of what's actually happening behind the interface.