Vasu Patel logoVasu Patel
← Patterns of Good Design
All chapters in this track
  1. 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
  2. 02Chapter 2: Patterns and SOLID: How They Relate
  3. 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
  4. 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
  5. 05Chapter 5: Structural Patterns I: Adapter & Facade
  6. 06Chapter 6: Structural Patterns II: Decorator & Proxy
  7. 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
  8. 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
  9. 09Chapter 9: Behavioral Patterns II: Observer & Mediator
  10. 10Chapter 10: Behavioral Patterns III: Command & Memento
  11. 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
  12. 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
  13. 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
  14. 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
  15. 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
  16. 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
  17. 17Chapter 17: Architectural Patterns III: The Repository Pattern
  18. 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

  1. 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.
  2. 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.
  3. 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 of get()) is a better trade-off than perfect transparency.
  4. 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

  1. Ask: does the wrapped object still do its full job, with something added? → Decorator.
  2. Ask: does the wrapper control whether or how the wrapped object's job happens at all (deferred, denied, redirected, cached)? → Proxy.
  3. Check whether behaviors genuinely need to combine — if not, Decorator's flexibility isn't earning its keep, and a simpler subclass may do.
  4. 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

  1. "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.

  2. "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.

  3. "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.

  4. "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.