Decorator Pattern

intermediate 2-4 YOE lld design-pattern structural

The Decorator pattern lets us add new behavior to an object without changing its class. We wrap the original object with a new object that adds functionality, like stacking layers.

Think of it like ordering coffee. We start with a base coffee. Then we add milk (wrapper #1). Then sugar (wrapper #2). Then whipped cream (wrapper #3). Each addition wraps the previous one and adds its cost and description. The base coffee never changes.

The Problem It Solves

Let’s say we have a Notification class. Now we want to add logging, encryption, and rate-limiting. Without Decorator, we’d need:

  • LoggedNotification
  • EncryptedNotification
  • RateLimitedNotification
  • LoggedEncryptedNotification
  • LoggedRateLimitedNotification
  • EncryptedRateLimitedNotification
  • LoggedEncryptedRateLimitedNotification

That’s 7 classes for 3 features. Add a 4th feature and it explodes to 15. This is called class explosion and it’s a nightmare.

With Decorator, we just stack what we need: RateLimit(Encrypt(Log(notification))). Each feature is one class. We combine them like LEGO blocks.

How It Works

Decorator Wrapping
WhippedCream ($0.70)
Milk ($0.50)
BaseCoffee ($2.00)
Total: $2.00 + $0.50 + $0.70 = $3.20
Each layer delegates to the inner layer, then adds its own behavior

The key rules:

  1. The decorator implements the same interface as the object it wraps
  2. It holds a reference to the wrapped object
  3. It delegates to the wrapped object, then adds its own behavior

Coffee Shop Example

from abc import ABC, abstractmethod

# Component interface
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass
    @abstractmethod
    def description(self) -> str:
        pass

# Base coffee
class SimpleCoffee(Coffee):
    def cost(self): return 2.00
    def description(self): return "Simple coffee"

# Decorators -- each wraps a Coffee and adds behavior
class MilkDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee  # wrap the inner coffee

    def cost(self): return self._coffee.cost() + 0.50
    def description(self): return self._coffee.description() + ", milk"

class SugarDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

    def cost(self): return self._coffee.cost() + 0.25
    def description(self): return self._coffee.description() + ", sugar"

class WhippedCreamDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

    def cost(self): return self._coffee.cost() + 0.70
    def description(self): return self._coffee.description() + ", whipped cream"

# Stack decorators like LEGO blocks
coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
coffee = WhippedCreamDecorator(coffee)

print(coffee.description())  # Simple coffee, milk, sugar, whipped cream
print(f"${coffee.cost()}")   # $3.45
// Base coffee
class SimpleCoffee {
  cost() { return 2.00; }
  description() { return "Simple coffee"; }
}

// Decorators
class MilkDecorator {
  #coffee;
  constructor(coffee) { this.#coffee = coffee; }
  cost() { return this.#coffee.cost() + 0.50; }
  description() { return this.#coffee.description() + ", milk"; }
}

class SugarDecorator {
  #coffee;
  constructor(coffee) { this.#coffee = coffee; }
  cost() { return this.#coffee.cost() + 0.25; }
  description() { return this.#coffee.description() + ", sugar"; }
}

class WhippedCreamDecorator {
  #coffee;
  constructor(coffee) { this.#coffee = coffee; }
  cost() { return this.#coffee.cost() + 0.70; }
  description() { return this.#coffee.description() + ", whipped cream"; }
}

// Stack them up
let coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);

console.log(coffee.description()); // Simple coffee, milk, sugar, whipped cream
console.log(`$${coffee.cost()}`);  // $3.45
// Component interface
interface Coffee {
    double cost();
    String description();
}

class SimpleCoffee implements Coffee {
    public double cost() { return 2.00; }
    public String description() { return "Simple coffee"; }
}

// Base decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
    CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}

class MilkDecorator extends CoffeeDecorator {
    MilkDecorator(Coffee coffee) { super(coffee); }
    public double cost() { return coffee.cost() + 0.50; }
    public String description() { return coffee.description() + ", milk"; }
}

class SugarDecorator extends CoffeeDecorator {
    SugarDecorator(Coffee coffee) { super(coffee); }
    public double cost() { return coffee.cost() + 0.25; }
    public String description() { return coffee.description() + ", sugar"; }
}

class WhippedCreamDecorator extends CoffeeDecorator {
    WhippedCreamDecorator(Coffee coffee) { super(coffee); }
    public double cost() { return coffee.cost() + 0.70; }
    public String description() { return coffee.description() + ", whipped cream"; }
}

// Usage:
// Coffee coffee = new SimpleCoffee();
// coffee = new MilkDecorator(coffee);
// coffee = new SugarDecorator(coffee);
// coffee = new WhippedCreamDecorator(coffee);
// coffee.cost() → 3.45

Decorator vs Inheritance

DecoratorInheritance
WhenRuntimeCompile time
FlexibilityMix and match any combinationFixed class hierarchy
Class countOne class per featureOne class per combination
PrincipleCompositionInheritance

Decorator follows the Open/Closed Principle — we can add new behavior without modifying existing code.

Real-World Examples

  • Java I/O: BufferedReader(InputStreamReader(FileInputStream("file.txt"))) — classic decorator chain
  • Express.js middleware: each middleware wraps the next handler
  • Python decorators: @login_required wraps a function with auth checking (similar concept, different mechanism)

When to Use

  • Adding features to objects at runtime without changing their class
  • When inheritance would cause a class explosion
  • When we want to combine behaviors in any order
  • Logging, caching, encryption, compression wrappers

When NOT to Use

  • When the order of decorators matters a lot and is confusing — too many layers become hard to debug
  • When a simple subclass does the job (only one variation needed)
  • When the component interface is huge — every decorator has to implement every method

In simple language, Decorator is like wrapping a gift. Each layer of wrapping adds something — a bow, a ribbon, a tag. The gift inside never changes. We just keep adding layers on top. And the best part? We can choose any combination of wrapping we want.