The decorator pattern is a structural design pattern that attaches additional responsibilities to an object dynamically by wrapping it in a decorator that implements the same interface. The client treats the decorated object like the original; each decorator adds behavior before or after delegating to the wrapped instance. Decorators can be stacked, so cross-cutting concerns such as logging, retries, or caching compose without subclass explosion.
The problem it solves
A class that sends notifications might need logging in development, rate limiting in production, and retries when the provider is flaky. Subclassing for every combination, such as LoggingSmsNotifier, RetryingSmsNotifier, and LoggingRetryingSmsNotifier, grows quickly and couples features that should be independent.
The decorator pattern factors each concern into its own wrapper. Each decorator implements the same interface as the component it wraps, delegates the core operation, and adds its own behavior around the call. The client still depends on one interface; which wrappers are applied is a composition choice at construction time.
Structure
- Component, the interface shared by the concrete object and all decorators.
- Concrete component, the object that carries the core behavior.
- Decorator, implements the component interface, holds a reference to another component, and forwards calls after adding behavior.
- Client, works against the component interface, unaware of how many decorators wrap the inner object.
flowchart LR Client["Client"] -->|"depends on"| Component["Component interface"] Logging["LoggingDecorator"] -.->|"implements"| Component Retry["RetryDecorator"] -.->|"implements"| Component Logging -->|"wraps"| Retry Retry -->|"wraps"| Sms["ConcreteComponent (SmsNotifier)"]
In code
A service sends SMS through a Notifier interface. Logging and retry behavior are added by wrapping the base notifier in decorators. The caller still calls send on the outermost wrapper.
// Component: shared interface
class SmsNotifier {
send(recipient, message) {
console.log(`SMS to ${recipient}: ${message}`);
}
}
// Decorator: adds logging around any notifier
class LoggingNotifier {
constructor(wrappee) {
this.wrappee = wrappee;
}
send(recipient, message) {
console.log(`[log] send to ${recipient}`);
this.wrappee.send(recipient, message);
}
}
// Decorator: retries on failure
class RetryNotifier {
constructor(wrappee, maxAttempts = 3) {
this.wrappee = wrappee;
this.maxAttempts = maxAttempts;
}
send(recipient, message) {
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
try {
this.wrappee.send(recipient, message);
return;
} catch (err) {
if (attempt === this.maxAttempts) throw err;
console.log(`[retry] attempt ${attempt} failed`);
}
}
}
}
// Client depends only on the component interface
function notifyUser(notifier, recipient, message) {
notifier.send(recipient, message);
}
const notifier = new LoggingNotifier(new RetryNotifier(new SmsNotifier()));
notifyUser(notifier, "+15551234567", "Your order shipped");// Component: shared interface
interface Notifier {
send(recipient: string, message: string): void;
}
class SmsNotifier implements Notifier {
send(recipient: string, message: string): void {
console.log(`SMS to ${recipient}: ${message}`);
}
}
// Decorator: adds logging around any notifier
class LoggingNotifier implements Notifier {
constructor(private readonly wrappee: Notifier) {}
send(recipient: string, message: string): void {
console.log(`[log] send to ${recipient}`);
this.wrappee.send(recipient, message);
}
}
// Decorator: retries on failure
class RetryNotifier implements Notifier {
constructor(
private readonly wrappee: Notifier,
private readonly maxAttempts = 3
) {}
send(recipient: string, message: string): void {
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
try {
this.wrappee.send(recipient, message);
return;
} catch (err) {
if (attempt === this.maxAttempts) throw err;
console.log(`[retry] attempt ${attempt} failed`);
}
}
}
}
// Client depends only on the component interface
function notifyUser(notifier: Notifier, recipient: string, message: string): void {
notifier.send(recipient, message);
}
const notifier = new LoggingNotifier(new RetryNotifier(new SmsNotifier()));
notifyUser(notifier, "+15551234567", "Your order shipped");// Component: shared interface
public interface INotifier
{
void Send(string recipient, string message);
}
public class SmsNotifier : INotifier
{
public void Send(string recipient, string message) =>
Console.WriteLine($"SMS to {recipient}: {message}");
}
// Decorator: adds logging around any notifier
public class LoggingNotifier : INotifier
{
private readonly INotifier _wrappee;
public LoggingNotifier(INotifier wrappee) => _wrappee = wrappee;
public void Send(string recipient, string message)
{
Console.WriteLine($"[log] send to {recipient}");
_wrappee.Send(recipient, message);
}
}
// Decorator: retries on failure
public class RetryNotifier : INotifier
{
private readonly INotifier _wrappee;
private readonly int _maxAttempts;
public RetryNotifier(INotifier wrappee, int maxAttempts = 3)
{
_wrappee = wrappee;
_maxAttempts = maxAttempts;
}
public void Send(string recipient, string message)
{
for (var attempt = 1; attempt <= _maxAttempts; attempt++)
{
try
{
_wrappee.Send(recipient, message);
return;
}
catch when (attempt < _maxAttempts)
{
Console.WriteLine($"[retry] attempt {attempt} failed");
}
}
}
}
// Client depends only on the component interface
void NotifyUser(INotifier notifier, string recipient, string message) =>
notifier.Send(recipient, message);
var notifier = new LoggingNotifier(new RetryNotifier(new SmsNotifier()));
NotifyUser(notifier, "+15551234567", "Your order shipped");from typing import Protocol
# Component: shared interface
class Notifier(Protocol):
def send(self, recipient: str, message: str) -> None: ...
class SmsNotifier:
def send(self, recipient: str, message: str) -> None:
print(f"SMS to {recipient}: {message}")
# Decorator: adds logging around any notifier
class LoggingNotifier:
def __init__(self, wrappee: Notifier) -> None:
self._wrappee = wrappee
def send(self, recipient: str, message: str) -> None:
print(f"[log] send to {recipient}")
self._wrappee.send(recipient, message)
# Decorator: retries on failure
class RetryNotifier:
def __init__(self, wrappee: Notifier, max_attempts: int = 3) -> None:
self._wrappee = wrappee
self._max_attempts = max_attempts
def send(self, recipient: str, message: str) -> None:
for attempt in range(1, self._max_attempts + 1):
try:
self._wrappee.send(recipient, message)
return
except Exception:
if attempt == self._max_attempts:
raise
print(f"[retry] attempt {attempt} failed")
# Client depends only on the component interface
def notify_user(notifier: Notifier, recipient: str, message: str) -> None:
notifier.send(recipient, message)
notifier = LoggingNotifier(RetryNotifier(SmsNotifier()))
notify_user(notifier, "+15551234567", "Your order shipped")Decorator vs subclassing
Subclassing fixes behavior at compile time: LoggingSmsNotifier is always logging plus SMS. Decorators compose at runtime. The same SmsNotifier can be wrapped with logging today and with caching tomorrow, or both, without new subclasses for each pair. That aligns with the open/closed principle from SOLID: extend behavior by adding decorators, not by editing or multiplying base classes.
Relationship to SOLID
Decorator supports the SOLID principles. It applies single responsibility by isolating each cross-cutting concern in its own class. It supports open/closed because new behavior is added through new decorators rather than changes to the concrete component. It also follows dependency inversion when the client depends on the component abstraction and receives whatever stack of decorators was wired in.
Decorator vs alternatives
- Adapter pattern, an adapter changes the interface so incompatible types can collaborate; a decorator keeps the same interface and adds behavior around it.
- Strategy pattern, a strategy replaces one algorithm with another behind a shared interface; a decorator wraps an object and typically delegates to it while adding layers. Strategies are usually interchangeable alternatives; decorators stack.
- Facade pattern, a facade defines a simpler API over many subsystem classes; a decorator wraps one object and preserves its interface.
- Proxy pattern, a proxy also wraps one object with the same interface, but controls access to the subject rather than adding behavior on every call.
- Chain of responsibility, handlers pass a request along a chain until one handles it; decorators always delegate through the stack and each adds behavior to the same operation.
Trade-offs
- Order matters when decorators wrap one another. Logging outside retries sees every attempt; logging inside retries sees only the final outcome. Document or standardize wrapping order for a given stack.
- Deep stacks of decorators can make debugging harder; each layer adds indirection, and the call path is not obvious from the outer type alone.
- Many languages ship decorator-like utilities (middleware pipelines,
IOstreams, aspect-oriented frameworks) that solve the same composition problem with less boilerplate. Use the pattern when you need explicit, type-safe wrapping in application code. - Best when behavior should be mixed and matched at runtime, when subclassing would produce a combinatorial explosion of classes, or when cross-cutting concerns should stay separate from the core implementation.
