Decorator Pattern: Add Capability in Layers

Decorator wraps an object with optional layers, allowing capabilities to be combined at runtime without creating a subclass for every combination.

What is the Decorator pattern?

The Decorator pattern is a structural design pattern that attaches new responsibilities to an object by wrapping it in another object that implements the same interface. Each wrapper adds exactly one capability — encryption, buffering, logging, retry — and then delegates to the object underneath. Because wrapper and wrapped share a contract, layers stack in any combination at runtime.

Decorator exists to kill the subclass explosion. With four optional features, inheritance needs up to sixteen subclasses to cover the combinations; decorators need four small classes composed as needed. Java's I/O streams (`BufferedInputStream(new FileInputStream(...))`) and middleware pipelines are decorator chains you have probably already used.

The logic, step by step

Problem

Optional features multiply into a subclass explosion because every possible combination requires another specialized type.

Rule

Wrap the same interface with focused decorators that add one responsibility before or after delegation.

Run
  1. BASE OBJECT provides the smallest valid behavior behind one interface.
  2. DECORATORS wrap that interface and add a focused responsibility.
  3. CALLS flow through the selected layers and finally reach the base.
Output

Features become composable layers rather than a rigid inheritance tree full of combinations.

Decorator pattern example in TypeScript

TypeScript
interface Notifier {
  send(message: string): void;
}

class EmailNotifier implements Notifier {
  send(message: string): void {
    smtp.deliver(message);
  }
}

class EncryptedNotifier implements Notifier {
  constructor(private inner: Notifier) {}
  send(message: string): void {
    this.inner.send(encrypt(message)); // add one concern, then delegate
  }
}

class ThrottledNotifier implements Notifier {
  private last = 0;
  constructor(private inner: Notifier, private minGapMs: number) {}
  send(message: string): void {
    const now = Date.now();
    if (now - this.last < this.minGapMs) return;
    this.last = now;
    this.inner.send(message);
  }
}

// Compose only the layers this deployment needs:
const notifier: Notifier =
  new ThrottledNotifier(new EncryptedNotifier(new EmailNotifier()), 5000);
notifier.send('Balance updated');
Throttling wraps encryption wraps email. A deployment that needs plain email uses the base class alone — no subclass for every combination.

Run it in the real world

A notification begins as plain text, then gains encryption, compression, and logging only where those layers are required.

When to use it — and when not to

Reach for Decorator when

  • Optional capabilities need to combine freely (compression + encryption + logging, in any subset).
  • You must add behavior to objects you cannot or should not subclass, including third-party classes.
  • Responsibilities should be attachable and removable at runtime rather than fixed at compile time.

Skip it when

  • The stack of wrappers is fixed and global — a single class or a function pipeline is simpler.
  • Consumers need to reach a specific layer's API; digging through wrappers breaks the abstraction.

Adapter pattern — Adapter changes an interface into a different one; Decorator keeps the interface identical and adds behavior. If the shape changes, it is an adapter; if the shape survives, it is a decorator.

Facade pattern — Facade simplifies many objects behind one new interface; Decorator enriches one object behind its existing interface.

Frequently asked questions

What is a decorator pattern in simple terms?

It is gift-wrapping for objects: each layer of wrapping adds one feature and passes the work inward, and the outside world still sees the same shape it started with.

Are TypeScript/Python @decorators the same as the Decorator pattern?

They are related but not identical. Language-level decorators are syntax that transforms a class or function at definition time. The design pattern composes wrapper objects at runtime. You can implement the pattern with the syntax, but most @decorators do other things.

When does the Decorator pattern become a problem?

Deep chains make debugging painful — a stack trace through six wrappers obscures where behavior lives, and identity checks (instanceof, equality) break because the outer object is not the inner one.