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:
LoggedNotificationEncryptedNotificationRateLimitedNotificationLoggedEncryptedNotificationLoggedRateLimitedNotificationEncryptedRateLimitedNotificationLoggedEncryptedRateLimitedNotification
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
Each layer delegates to the inner layer, then adds its own behavior
The key rules:
- The decorator implements the same interface as the object it wraps
- It holds a reference to the wrapped object
- 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
| Decorator | Inheritance | |
|---|---|---|
| When | Runtime | Compile time |
| Flexibility | Mix and match any combination | Fixed class hierarchy |
| Class count | One class per feature | One class per combination |
| Principle | Composition | Inheritance |
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_requiredwraps 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.