Structural Pattern Pattern Deep Dive

Decorator

Some objects need optional extra behavior, and the options combine — milk alone, sugar alone, milk and sugar, milk and sugar and cream. The Decorator pattern answers the question: how do you add combinable, stackable behavior to an individual object at runtime, without writing a subclass for every possible combination?

⚠️ The Problem

Model a coffee shop's orders with subclasses — Coffee, then CoffeeWithMilk, then CoffeeWithMilkAndSugar, then CoffeeWithMilkAndSugarAndCream — and you're building a subclass for every combination of optional add-ons a customer might order. Three independent add-ons (milk, sugar, cream) already means up to eight combinations; a fourth add-on doubles that again. The subclass count grows exponentially with the number of options, and most of those classes differ from each other only in which add-ons' price and description got tacked on.

✅ The Solution

Give the base object and every add-on the same interface (e.g. cost() and description()). Each add-on is a Decorator that wraps another object implementing that interface, adds its own cost/description, and delegates everything else to the object it wraps. To build "coffee with milk and sugar," wrap a plain Coffee in a MilkDecorator, then wrap that in a SugarDecorator — decorators stack in any combination, chosen at runtime, with no new class needed per combination.

Example 1 — a coffee order with milk, sugar, and cream

Primary Example — JavaScript
// Shared interface: every coffee, plain or decorated, has cost() and description().
class Coffee {
  cost() { return 2.0; }
  description() { return "Coffee"; }
}

// Base class for decorators — wraps another Coffee-shaped object.
class CoffeeDecorator {
  constructor(wrapped) {
    this.wrapped = wrapped;
  }
  cost() { return this.wrapped.cost(); }
  description() { return this.wrapped.description(); }
}

class MilkDecorator extends CoffeeDecorator {
  cost() { return this.wrapped.cost() + 0.5; }
  description() { return this.wrapped.description() + " + milk"; }
}

class SugarDecorator extends CoffeeDecorator {
  cost() { return this.wrapped.cost() + 0.25; }
  description() { return this.wrapped.description() + " + sugar"; }
}

class WhippedCreamDecorator extends CoffeeDecorator {
  cost() { return this.wrapped.cost() + 0.75; }
  description() { return this.wrapped.description() + " + whipped cream"; }
}

// Stack decorators in any combination, decided at runtime by what the customer orders.
let order = new Coffee();
order = new MilkDecorator(order);
order = new SugarDecorator(order);
order = new WhippedCreamDecorator(order);

console.log(order.description()); // "Coffee + milk + sugar + whipped cream"
console.log(order.cost().toFixed(2)); // "3.50"

No CoffeeWithMilkAndSugarAndCream class exists anywhere. Each decorator only knows about the one thing it adds, and any subset of decorators can wrap the base Coffee in any order the customer's order requires.

Example 2 — wrapping a data stream with compression and encryption

Secondary Example — Python
from abc import ABC, abstractmethod

# Shared interface: every stream, plain or decorated, can write(data).
class DataStream(ABC):
    @abstractmethod
    def write(self, data: str) -> str:
        ...

class FileStream(DataStream):
    def write(self, data: str) -> str:
        return f"writing raw bytes: {data}"

# Base decorator: wraps another DataStream and delegates by default.
class StreamDecorator(DataStream):
    def __init__(self, wrapped: DataStream):
        self._wrapped = wrapped

    def write(self, data: str) -> str:
        return self._wrapped.write(data)

class CompressionDecorator(StreamDecorator):
    def write(self, data: str) -> str:
        compressed = f"gzip({data})"
        return self._wrapped.write(compressed)

class EncryptionDecorator(StreamDecorator):
    def write(self, data: str) -> str:
        encrypted = f"aes256({data})"
        return self._wrapped.write(encrypted)

# Stack decorators in any order, at runtime, based on what a given upload needs.
stream: DataStream = FileStream()
stream = CompressionDecorator(stream)
stream = EncryptionDecorator(stream)

print(stream.write("user-report.csv"))
# writing raw bytes: aes256(gzip(user-report.csv))

Compression and encryption are independent, stackable concerns — exactly like milk and sugar — just applied to a data stream instead of a drink. Either decorator can be added, removed, or reordered without touching FileStream or the other decorator's code.

👍 When To Use It

  • You need optional, combinable behavior on individual objects, and the number of combinations would otherwise require a subclass per combination.
  • You want to add or remove behavior at runtime, per instance — not fixed at compile time for an entire class of objects.
  • You want each concern (logging, caching, compression, formatting) to live in its own small, independently testable class, rather than all mixed into one growing base class.

👎 When NOT To Use It

  • When the behavior applies to every instance of a type, always, with no need to toggle it — a normal subclass or a flag on the base class is simpler.
  • When decorators must be applied in a very specific order for correctness and that order isn't obvious from the code — deeply nested decorator stacks can get hard to reason about and debug.
  • When you need to inspect or depend on the concrete wrapped type — decorators deliberately hide what's underneath behind the shared interface, which can complicate code that needs to "see through" the wrapping.

❓ FAQ

How is Decorator different from just using inheritance/subclassing?

Inheritance adds behavior at compile time, to every instance of a subclass — if you make a CoffeeWithMilk class, every object of that class always has milk. Decorator adds behavior at runtime, to specific instances only — the same Coffee object can be wrapped with milk today and left plain tomorrow, and two different orders can be decorated completely differently even though they started from the same base class.

Isn't this the same as the Adapter pattern? Both wrap an object.

Both wrap, but for different reasons. Adapter wraps to translate an incompatible interface into one the client already expects — the interface changes, the behavior doesn't. Decorator wraps an object that already implements the interface the client expects, and adds new behavior on top while keeping that same interface — the interface stays the same, the behavior grows.

Does order matter when stacking decorators?

Often yes. In the stream example, compressing then encrypting produces different bytes than encrypting then compressing (and typically only one order is actually useful — encrypted data usually doesn't compress well). Decorator gives you the flexibility to stack in any order; it's still up to you to stack them in an order that makes sense for the problem.

🙋 There Are No Dumb Questions

Q: If I wrap an object three times, how do I get back the original, undecorated object?
A: Generally you don't, and that's by design — each decorator only holds a reference to the thing it wraps, not a way to strip itself off. If you need to reconstruct or inspect the original later, keep a separate reference to the undecorated object before you start wrapping it, rather than trying to "unwrap" a decorator chain after the fact.

✅ Quick Check

What is the key advantage of Decorator over subclassing for every combination of optional behavior?


Next up: what happens when a subsystem is technically correct but painful to use directly — like video conversion involving codecs, audio mixing, and metadata all in a specific order? That's the Facade pattern.

← Back to
Next →