Decorator
Attaches additional responsibilities to an object dynamically, wrapping it in a wrapper that implements the same interface — a flexible alternative to inheritance for extending behavior.
Intent
Add responsibilities to an object dynamically, without altering its class. The Decorator wraps the original object in a wrapper that implements the same interface — delegating calls to the inner object and adding behavior before, after, or around the delegation. Decorators can be stacked in any order and combination.
Cataloged by the GoF (1994) as a structural pattern, the Decorator is the elegant answer to the subclass explosion problem: when you need varied combinations of behavior, creating a subclass for every combination scales unsustainably.
Problem
Consider a notification system that needs to support optional features: adding a timestamp to the message, logging the notification, and encrypting the content before sending. These features can be combined freely — with or without timestamp, with or without logging, with or without encryption.
With an inheritance approach, you would need a subclass for every
combination: NotifierWithTimestamp,
NotifierWithLog, NotifierWithTimestampAndLog,
NotifierWithEncryption,
NotifierWithTimestampAndEncryption, and so on. With just
three optional features, that's up to 8 possible combinations (2³).
With four, it's 16. The growth is exponential.
Moreover, inheritance is static — the combination of behaviors is fixed at compile time and can't be changed at runtime as needs change.
Decorator vs Adapter: the key difference
It's important not to confuse the two patterns. The Adapter changes an object's interface — its goal is compatibility between incompatible interfaces. The Decorator keeps exactly the same interface as the object it wraps — its goal is to add behavior to the same contract. If you see a wrapper that changes the interface, it's an Adapter; if it keeps the interface and adds functionality, it's a Decorator.
Solution
The Decorator organizes the code into four participants:
-
Component (interface): defines the contract shared
between the concrete object and all decorators. E.g.:
Notifierinterface with the methodsend(message). -
ConcreteComponent: the base implementation, without
decorations. E.g.:
EmailNotifier— sends the e-mail and nothing else. - Decorator (abstract base class, optional): implements Component and keeps a reference to another Component (the wrapped object). Delegates all calls to the inner object. In TypeScript, this can be an abstract class or simply have each concrete decorator keep the reference itself.
-
ConcreteDecorator: extends the Decorator and adds
behavior before, after, or around the delegated call. E.g.:
TimestampNotifier,LoggingNotifier.
The key is that the result of a decorator is itself a Component —
which allows stacking decorators freely:
new LoggingNotifier(new TimestampNotifier(new EmailNotifier())).
Structure
«interface»
Notifier
┌───────────────────┐
│ + send(msg): void │
└───────────────────┘
▲ ▲
│ │ implements (and wraps another Notifier)
EmailNotifier NotifierDecorator (abstract base)
(ConcreteComp.) ┌───────────────────────────────┐
│ # wrapped: Notifier │
│ + send(msg): void │
│ → delegates to this.wrapped │
└───────────────────────────────┘
▲
┌───────────┴──────────┐
│ │
TimestampNotifier LoggingNotifier
(ConcreteDecorator) (ConcreteDecorator)
send(msg): send(msg):
msg = "[ts] " + msg this.wrapped.send(msg)
this.wrapped.send(msg) logMessage(msg)
Stacking (right to left in the code):
new LoggingNotifier(
new TimestampNotifier(
new EmailNotifier()
)
)
→ send("Hello") calls:
1. LoggingNotifier.send → logs the message
2. TimestampNotifier.send → prepends timestamp
3. EmailNotifier.send → sends the e-mail
Code examples
Example 1 — Notifier with optional layers
Each decorator adds one responsibility. The combination and the order are defined at runtime — without creating subclasses for every combination.
// ── Component ─────────────────────────────────────────────────
interface Notifier {
send(message: string): void;
}
// ── ConcreteComponent ─────────────────────────────────────────
class EmailNotifier implements Notifier {
send(message: string): void {
console.log(`[EMAIL] ${message}`);
}
}
// ── Decorators ────────────────────────────────────────────────
// Adds a timestamp to the message before delegating.
class TimestampNotifier implements Notifier {
constructor(private readonly wrapped: Notifier) {}
send(message: string): void {
const ts = new Date().toISOString();
this.wrapped.send(`[${ts}] ${message}`);
}
}
// Logs the message before delegating.
class LoggingNotifier implements Notifier {
private readonly history: string[] = [];
constructor(private readonly wrapped: Notifier) {}
send(message: string): void {
this.history.push(message);
console.log(`[LOG] Message logged. Total: ${this.history.length}`);
this.wrapped.send(message);
}
getHistory(): readonly string[] {
return this.history;
}
}
// Simulates simple encryption before delegating.
class EncryptingNotifier implements Notifier {
constructor(private readonly wrapped: Notifier) {}
send(message: string): void {
// Simulation: we reverse the string as "encryption"
const encrypted = message.split("").reverse().join("");
this.wrapped.send(`[ENC:${encrypted}]`);
}
}
// ── Usage — composing decorators at runtime ───────────────────
const base = new EmailNotifier();
// Timestamp only:
const withTimestamp = new TimestampNotifier(base);
withTimestamp.send("Your invoice has arrived");
// [EMAIL] [2026-06-25T...] Your invoice has arrived
// Log + timestamp (log is the outermost):
const loggedNotifier = new LoggingNotifier(
new TimestampNotifier(base)
);
loggedNotifier.send("Order confirmed");
// [LOG] Message logged. Total: 1
// [EMAIL] [2026-06-25T...] Order confirmed
// All three, in order: log → encryption → timestamp → email
const full = new LoggingNotifier(
new EncryptingNotifier(
new TimestampNotifier(base)
)
);
full.send("Hi");
// [LOG] Message logged. Total: 1
// [EMAIL] [2026-06-25T...] [ENC:iH]
<?php
// ── Component ─────────────────────────────────────────────────
interface Notifier
{
public function send(string $message): void;
}
// ── ConcreteComponent ─────────────────────────────────────────
class EmailNotifier implements Notifier
{
public function send(string $message): void
{
echo "[EMAIL] {$message}" . PHP_EOL;
}
}
// ── Decorators ────────────────────────────────────────────────
// Adds a timestamp to the message before delegating.
class TimestampNotifier implements Notifier
{
public function __construct(private readonly Notifier $wrapped) {}
public function send(string $message): void
{
$ts = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM);
$this->wrapped->send("[{$ts}] {$message}");
}
}
// Logs the message before delegating.
class LoggingNotifier implements Notifier
{
/** @var string[] */
private array $history = [];
public function __construct(private readonly Notifier $wrapped) {}
public function send(string $message): void
{
$this->history[] = $message;
$total = count($this->history);
echo "[LOG] Message logged. Total: {$total}" . PHP_EOL;
$this->wrapped->send($message);
}
/** @return string[] */
public function getHistory(): array
{
return $this->history;
}
}
// Simulates simple encryption before delegating.
class EncryptingNotifier implements Notifier
{
public function __construct(private readonly Notifier $wrapped) {}
public function send(string $message): void
{
$encrypted = strrev($message);
$this->wrapped->send("[ENC:{$encrypted}]");
}
}
// ── Usage — composing decorators at runtime ───────────────────
$base = new EmailNotifier();
// Timestamp only:
$withTimestamp = new TimestampNotifier($base);
$withTimestamp->send('Your invoice has arrived');
// [EMAIL] [2026-06-25T...] Your invoice has arrived
// Log + timestamp:
$loggedNotifier = new LoggingNotifier(
new TimestampNotifier($base)
);
$loggedNotifier->send('Order confirmed');
// [LOG] Message logged. Total: 1
// [EMAIL] [2026-06-25T...] Order confirmed
// All three:
$full = new LoggingNotifier(
new EncryptingNotifier(
new TimestampNotifier($base)
)
);
$full->send('Hi');
// [LOG] Message logged. Total: 1
// [EMAIL] [2026-06-25T...] [ENC:iH]
Example 2 — Beverage price with add-ons (classic GoF example)
The coffee-with-add-ons example is canonical in the Decorator literature. Each add-on (milk, caramel, whipped cream) is a decorator that adds its price to the total of the wrapped component. The final result correctly accumulates all values.
// ── Component ─────────────────────────────────────────────────
interface Beverage {
description(): string;
price(): number; // in cents
}
// ── ConcreteComponents ────────────────────────────────────────
class Espresso implements Beverage {
description(): string { return "Espresso"; }
price(): number { return 300; } // $3.00
}
class Americano implements Beverage {
description(): string { return "Americano"; }
price(): number { return 200; } // $2.00
}
// ── Add-on decorators ─────────────────────────────────────────
class WithMilk implements Beverage {
constructor(private readonly beverage: Beverage) {}
description(): string { return `${this.beverage.description()}, Milk`; }
price(): number { return this.beverage.price() + 50; } // +$0.50
}
class WithCaramel implements Beverage {
constructor(private readonly beverage: Beverage) {}
description(): string { return `${this.beverage.description()}, Caramel`; }
price(): number { return this.beverage.price() + 75; } // +$0.75
}
class WithWhippedCream implements Beverage {
constructor(private readonly beverage: Beverage) {}
description(): string { return `${this.beverage.description()}, Whipped Cream`; }
price(): number { return this.beverage.price() + 100; } // +$1.00
}
// ── Usage ───────────────────────────────────────────────────────
const plain = new Espresso();
console.log(`${plain.description()}: $${(plain.price() / 100).toFixed(2)}`);
// Espresso: $3.00
const cappuccino = new WithWhippedCream(new WithMilk(new Espresso()));
console.log(`${cappuccino.description()}: $${(cappuccino.price() / 100).toFixed(2)}`);
// Espresso, Milk, Whipped Cream: $4.50
// (300 + 50 + 100 = 450 cents = $4.50 ✓)
const special = new WithCaramel(new WithWhippedCream(new WithMilk(new Americano())));
console.log(`${special.description()}: $${(special.price() / 100).toFixed(2)}`);
// Americano, Milk, Whipped Cream, Caramel: $4.25
// (200 + 50 + 100 + 75 = 425 cents = $4.25 ✓)
<?php
// ── Component ─────────────────────────────────────────────────
interface Beverage
{
public function description(): string;
public function price(): int; // in cents
}
// ── ConcreteComponents ────────────────────────────────────────
class Espresso implements Beverage
{
public function description(): string { return 'Espresso'; }
public function price(): int { return 300; } // $3.00
}
class Americano implements Beverage
{
public function description(): string { return 'Americano'; }
public function price(): int { return 200; } // $2.00
}
// ── Add-on decorators ─────────────────────────────────────────
class WithMilk implements Beverage
{
public function __construct(private readonly Beverage $beverage) {}
public function description(): string { return $this->beverage->description() . ', Milk'; }
public function price(): int { return $this->beverage->price() + 50; } // +$0.50
}
class WithCaramel implements Beverage
{
public function __construct(private readonly Beverage $beverage) {}
public function description(): string { return $this->beverage->description() . ', Caramel'; }
public function price(): int { return $this->beverage->price() + 75; } // +$0.75
}
class WithWhippedCream implements Beverage
{
public function __construct(private readonly Beverage $beverage) {}
public function description(): string { return $this->beverage->description() . ', Whipped Cream'; }
public function price(): int { return $this->beverage->price() + 100; } // +$1.00
}
// ── Usage ───────────────────────────────────────────────────────
$plain = new Espresso();
printf("%s: $%.2f\n", $plain->description(), $plain->price() / 100);
// Espresso: $3.00
$cappuccino = new WithWhippedCream(new WithMilk(new Espresso()));
printf("%s: $%.2f\n", $cappuccino->description(), $cappuccino->price() / 100);
// Espresso, Milk, Whipped Cream: $4.50
// (300 + 50 + 100 = 450 cents = $4.50 ✓)
$special = new WithCaramel(new WithWhippedCream(new WithMilk(new Americano())));
printf("%s: $%.2f\n", $special->description(), $special->price() / 100);
// Americano, Milk, Whipped Cream, Caramel: $4.25
// (200 + 50 + 100 + 75 = 425 cents = $4.25 ✓)
When to use
- When you need to add responsibilities to objects dynamically and reversibly, without modifying the original class.
- To avoid subclass explosion in scenarios where behaviors are optional and combined freely (e.g.: logging + auth + caching middleware).
-
When extension via inheritance is impractical
because the class is
final, or because the possible combinations are too many to model statically. - Pipelines and middleware: the middleware stack in Express, Laravel, ASP.NET and similar frameworks is a direct application of the Decorator — each middleware is a decorator over the next handler.
When to avoid
- When there's a single fixed additional behavior: a simple subclass or direct composition is more readable than building the full Decorator structure.
- When the order of decorators is sensitive and undocumented: decorator stacks can produce unexpected behavior if assembled in the wrong order. If this is frequent, consider a Builder to construct the stack in a controlled way.
-
When you need to reference the concrete component:
code that downcasts to obtain the
ConcreteComponentfrom inside the decorator stack breaks encapsulation and invalidates the pattern.
Pros and cons
Pros
- Extends the behavior of objects without altering the original class (Open/Closed Principle).
- Combines responsibilities at runtime — much more flexible than static inheritance.
- Avoids huge class hierarchies to cover every possible combination.
- Each decorator has a single responsibility — easier to test in isolation.
- Decorators can be reused in different combinations and over different components.
Cons
- Deep decorator stacks make debugging hard — tracing which decorator caused an unexpected behavior requires inspecting the whole chain.
- The order of decorators matters and can be counterintuitive.
- Large interfaces (many methods) are cumbersome to decorate — each decorator must implement every method, even if it only overrides one.
- It can be hard to remove a specific decorator from the middle of a stack.
Common pitfalls
1. Confusing Decorator with Adapter
The essential distinction: the Decorator keeps the same interface as the object it wraps. If the wrapper returns a different interface from the object it wraps, it's an Adapter, not a Decorator. When you notice the wrapper is "translating" calls to a different API, reconsider whether the correct pattern isn't Adapter.
2. Decorators with shared state
Warning: each decorator instance has its own state
(like the history of LoggingNotifier). If the
same decorator is reused across different contexts without being
reinstantiated, accumulated state from one context can leak into
another. Prefer stateless decorators whenever possible; when state is
needed, clearly document the expected lifecycle.
3. Large interfaces are hard to decorate
If the Component interface has 10 methods, each decorator must
implement all 10 — even if it only wants to override 1. This creates
repetitive delegation code. The classic solution is to create an
abstract Decorator base class that delegates all methods to
wrapped, letting the concrete decorators override only
what they need.
4. Order matters more than it seems
In Example 1, new LoggingNotifier(new TimestampNotifier(base))
logs the message without a timestamp (because the log is the
outermost layer). Reversing the order —
new TimestampNotifier(new LoggingNotifier(base)) — would
make the log record the message with a timestamp. It's not
always obvious which order is the intended one. Document the expected
order and, if necessary, use a Builder to guarantee the correct
assembly.
Related patterns
The Decorator interacts with several patterns, especially the other structural ones:
The Adapter changes the interface; the Decorator keeps the interface and adds behavior — that's the fundamental difference between the two. The Facade simplifies access to a subsystem; a Facade can be internally built with Decorators over the subsystem's components. The Strategy also varies behavior, but by substitution: it swaps the entire algorithm for a different one (the context chooses which Strategy to use). The Decorator accumulates behaviors in layers — it doesn't replace, it adds. The Composite shares the same recursive structure (an object that contains others of the same type), but for the purpose of treating a group of objects as one, not adding responsibilities. The Proxy also wraps an object keeping the same interface, but with the goal of controlling access (lazy loading, caching, security) — not adding business responsibilities.