Decorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
Overview
The Decorator Pattern is a structural design pattern that lets you attach new behaviors to objects by placing them inside wrapper objects that contain the behaviors. It provides a flexible alternative to subclassing for extending functionality.
It is widely used in I/O streams (Java), middleware pipelines (Express.js), and Python’s @decorator syntax.
When to Use
Use the Decorator Pattern when:
- You need to add responsibilities to objects dynamically and transparently
- Extension by subclassing is impractical or impossible (e.g., final classes)
- You want to combine multiple behaviors in various configurations
- You need to adhere to the Single Responsibility Principle by separating concerns
- You want to avoid a class explosion from subclassing every possible combination
Solution
Python
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Simple coffee"
class MilkDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost() + 0.5
def description(self) -> str:
return self._coffee.description() + ", milk"
# Usage
coffee = MilkDecorator(SimpleCoffee())
print(coffee.description()) # Simple coffee, milk
print(coffee.cost()) # 2.5
JavaScript
class Coffee {
cost() {
return 2.0;
}
description() {
return "Simple coffee";
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 0.5;
}
description() {
return this.coffee.description() + ", milk";
}
}
// Usage
const coffee = new MilkDecorator(new Coffee());
console.log(coffee.description()); // Simple coffee, milk
console.log(coffee.cost()); // 2.5
Java
interface Coffee {
double cost();
String description();
}
class SimpleCoffee implements Coffee {
public double cost() { return 2.0; }
public String description() { return "Simple coffee"; }
}
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.5; }
public String description() { return coffee.description() + ", milk"; }
}
// Usage
Coffee coffee = new MilkDecorator(new SimpleCoffee());
System.out.println(coffee.description()); // Simple coffee, milk
System.out.println(coffee.cost()); // 2.5
Explanation
The Decorator Pattern relies on composition over inheritance:
- Component Interface (
Coffee): Defines the contract for both concrete components and decorators - Concrete Component (
SimpleCoffee): The base object being wrapped - Decorator (
MilkDecorator): Implements the same interface and delegates to the wrapped object
Decorators can be nested arbitrarily. You can wrap a MilkDecorator with a SugarDecorator, then with a WhipDecorator, building behavior stacks at runtime.
Variants
| Variant | Use Case | Trade-off |
|---|---|---|
| Class-based | Strongly typed languages (Java, C#) | Verbose but type-safe |
| Function-based | Python @decorator syntax | Concise, but less explicit composition |
| Middleware pipeline | Web frameworks (Express, Koa) | Great for request/response processing |
What Works
- Keep decorators transparent: They should implement the exact same interface as the component
- Delegate all methods: Unless intentionally overriding, pass every call to the wrapped object
- Avoid stateful decorators when possible to reduce complexity
- Document decorator order: Some decorators may behave differently depending on wrapping order
- Prefer composition over inheritance: This is the core philosophy of the pattern
Common Mistakes
- Forgetting to delegate: A decorator that does not forward calls breaks the chain
- Leaky abstraction: Decorators exposing methods not in the component interface
- Order sensitivity: Decorators that depend on being inner or outer can cause subtle bugs
- Over-decoration: Too many nested decorators make debugging and profiling difficult
- State conflicts: Multiple decorators holding conflicting state about the same component
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply decorator pattern when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Advanced Topics
Scenario: Decorators for Logging and Cache
// Decorator pattern to add behavior without modifying code
interface DataService {
getData(key: string): Promise<unknown>;
}
class APIDataService implements DataService {
async getData(key: string): Promise<unknown> {
const res = await fetch(`/api/data/${key}`);
return res.json();
}
}
// Decorator: Logging
class LoggingDecorator implements DataService {
constructor(private wrapped: DataService) {}
async getData(key: string): Promise<unknown> {
const start = Date.now();
console.log(`[LOG] getData(${key}) started`);
try {
const result = await this.wrapped.getData(key);
console.log(`[LOG] getData(${key}) OK in ${Date.now() - start}ms`);
return result;
} catch (err) {
console.error(`[LOG] getData(${key}) FAILED: ${err}`);
throw err;
}
}
}
// Decorator: Cache
class CacheDecorator implements DataService {
private cache = new Map<string, { value: unknown; expiry: number }>();
constructor(private wrapped: DataService, private ttlMs: number) {}
async getData(key: string): Promise<unknown> {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[CACHE] HIT: ${key}`);
return cached.value;
}
console.log(`[CACHE] MISS: ${key}`);
const result = await this.wrapped.getData(key);
this.cache.set(key, { value: result, expiry: Date.now() + this.ttlMs });
return result;
}
}
// Composition: API + Cache + Logging
const service = new LoggingDecorator(
new CacheDecorator(
new APIDataService(),
60000 // 60s TTL
)
);
// Result: each call goes through logging -> cache -> API
// Cache HIT: does not call API
// Cache MISS: calls API and stores in cache
Lessons:
- Decorator adds behavior without modifying the original class
- Decorators compose: cache + logging + retry
- Order matters: cache outside logging to avoid logging hits
- Maintains Open/Closed: open for extension, closed for modification
- In TypeScript, use class decorators (@decorator) for metadata
### How do I order multiple decorators?
Order matters. Put the cheapest decorator outside (cache) and the most expensive inside (API). Logging outside cache: so you see both hits and misses. Retry inside logging but outside API: retries before failing. Typical order: logging -> cache -> retry -> API. Each decorator wraps the next, forming a chain of responsibility.
End of document. Review and update quarterly.
## Common Production Pitfalls
- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern's performance and error propagation. Frequently Asked Questions
What is the difference between Decorator and Proxy?
Decorator adds responsibilities dynamically. Proxy controls access to an object (lazy initialization, access control, logging). They have similar structure but different intent.
Can decorators be removed at runtime?
Not easily in most implementations. If you need add/remove flexibility, consider the Chain of Responsibility pattern instead.
Are Python's @decorator syntax and the Decorator Pattern the same?
Python's @decorator is a language feature for wrapping functions. The Decorator Pattern is an OOP design pattern for wrapping objects. They share the concept but apply to different levels.
Related Resources
Adapter Pattern
Convert the interface of a class into another interface clients expect. A structural design pattern for interface compatibility.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
RecipeCall a REST API: Python, JavaScript, Java & Go Examples
How to make HTTP requests to a REST API and handle the JSON response in Python, JavaScript, Java, and Go.
PatternBridge Pattern: Decouple Abstraction from Implementation
Split a class into two hierarchies — abstraction and implementation — so both can evolve independently. Includes Python, Java, and JavaScript examples.
PatternBuilder Pattern
Construct complex objects step by step. A creational design pattern for readable, configurable object construction.
PatternChain of Responsibility Pattern
Pass requests along a chain of handlers until one handles it. A behavioral design pattern for decoupling senders and receivers.