Decorator Pattern

Topics Covered

The Decorator Pattern

How It Works

The Four Roles

Wrapping Behavior Dynamically

Runtime Composition

Removing Decorators

Factory + Decorator

Decorators vs Inheritance

The Numbers

Beyond the Numbers

When Inheritance Wins

Stacking Decorators

Java I/O Streams

Order Matters

Practical Guidelines

Decorator in Real Systems

Web Middleware

Python's @decorator Syntax

Where Else You See It

Practice Problems

You want to add behavior to an object without changing its class. Inheritance seems like the obvious solution: create a subclass that adds the new behavior. But what if you need to add multiple optional behaviors in different combinations? With two options (milk, sugar), you need four subclasses. With five options, you need thirty-two. With ten, you need over a thousand. The number of classes explodes exponentially.

The Decorator pattern solves this by wrapping objects instead of subclassing them. Each decorator implements the same interface as the object it wraps, adds one piece of behavior, and delegates everything else. You compose behaviors by stacking decorators: each layer adds its contribution and passes the call down.

Decorator Wrapping Layers

How It Works

A coffee shop illustrates the pattern perfectly. Every beverage has a cost and a description. Decorators add toppings:

python
1class Beverage:
2    def cost(self) -> float:
3        raise NotImplementedError
4    def description(self) -> str:
5        raise NotImplementedError
6
7class Espresso(Beverage):
8    def cost(self) -> float:
9        return 2.00
10    def description(self) -> str:
11        return "Espresso"
12
13class MilkDecorator(Beverage):
14    def __init__(self, beverage: Beverage):
15        self._beverage = beverage
16    def cost(self) -> float:
17        return self._beverage.cost() + 0.50
18    def description(self) -> str:
19        return self._beverage.description() + ", Milk"
20
21class SugarDecorator(Beverage):
22    def __init__(self, beverage: Beverage):
23        self._beverage = beverage
24    def cost(self) -> float:
25        return self._beverage.cost() + 0.25
26    def description(self) -> str:
27        return self._beverage.description() + ", Sugar"

Building a drink with milk and sugar:

python
drink = SugarDecorator(MilkDecorator(Espresso()))
print(drink.description())  # "Espresso, Milk, Sugar"
print(drink.cost())         # 2.75

Each decorator wraps the previous one. The cost chains through all layers: Sugar asks Milk, Milk asks Espresso, Espresso returns 2.00, Milk adds 0.50, Sugar adds 0.25. The total is 2.75.

Key Insight

The decorator's power comes from a simple rule: every decorator implements the same interface as the object it wraps. This means the client cannot tell whether it is talking to the original object or a decorated version. The decoration is invisible.

The Four Roles

Every decorator implementation has four parts:

  1. Component interface: the shared interface (Beverage) that both concrete objects and decorators implement
  2. Concrete component: the base object being decorated (Espresso, Latte)
  3. Base decorator: an abstract class that holds a reference to a wrapped component and delegates by default
  4. Concrete decorators, classes that override specific methods to add behavior (MilkDecorator, SugarDecorator)

The base decorator is optional but useful: it provides default delegation so concrete decorators only need to override the methods where they add behavior.

The real advantage of decorators over inheritance is timing. Inheritance adds behavior at compile time: you decide the class hierarchy when you write the code, and it is fixed forever. Decorators add behavior at runtime: you decide what to wrap based on configuration, user input, or system state.

Runtime Composition

Consider a logging system where different environments need different processing:

python
1def create_logger(config):
2    logger = ConsoleLogger()
3
4    if config.get("file_logging"):
5        logger = FileWriterDecorator(logger)
6    if config.get("timestamps"):
7        logger = TimestampDecorator(logger)
8    if config.get("encryption"):
9        logger = EncryptionDecorator(logger)
10
11    return logger

In development, you might get a plain ConsoleLogger. In production, you get a logger wrapped with timestamps, file writing, and encryption: all from the same code with different configuration. With inheritance, you would need to predefine every possible combination as a separate class.

Removing Decorators

Since decorators are objects wrapping other objects, you can also remove them. A feature flag system might add a monitoring decorator when a feature is enabled and remove it when disabled: all without restarting the application.

This is impossible with inheritance. You cannot remove a parent class from an object at runtime. The class hierarchy is baked into the compiled code.

Factory + Decorator

A common pattern combines the Factory pattern with decorators. The factory decides which decorators to apply based on input, and the client receives a fully decorated object without knowing its structure:

python
1class BeverageFactory:
2    @staticmethod
3    def create(order: dict) -> Beverage:
4        drink = Espresso() if order["base"] == "espresso" else Latte()
5        for topping in order.get("toppings", []):
6            if topping == "milk":
7                drink = MilkDecorator(drink)
8            elif topping == "sugar":
9                drink = SugarDecorator(drink)
10        return drink

The factory encapsulates the decoration logic. The client calls BeverageFactory.create(order) and receives a Beverage: it does not know or care how many decorators are involved.

The subclass explosion problem is the clearest argument for decorators. But there are deeper reasons why decorators are often better than inheritance for extending behavior.

Subclass Explosion vs Composable Decorators

The Numbers

With inheritance, each combination of features requires its own subclass:

OptionsSubclasses needed
2 (milk, sugar)4
3 (+ whipped cream)8
5 (+ vanilla, caramel)32
10 options1,024

With decorators, you need exactly N decorator classes for N options. Any combination is built at runtime by stacking.

Beyond the Numbers

The subclass explosion is dramatic, but the real problem with inheritance is rigidity:

Adding a new option: With inheritance, you must create subclasses for every existing combination that includes the new option. With decorators, you write one new class.

Removing an option: With inheritance, you must find and delete every subclass that includes the removed option. With decorators, you remove one class and update the factory.

Changing the base: If the base class (Espresso) changes its cost() calculation, every subclass must be retested. With decorators, only the base class test changes: decorators are independent.

When Inheritance Wins

Decorators are not always better. Inheritance is simpler when:

  • The combinations are fixed and few (a Shape hierarchy with Circle, Rectangle, Triangle)
  • The behavior extension is tightly coupled to the base class's internals
  • The extension needs access to protected members
Interview Tip

When an interviewer asks about extending behavior, ask yourself: is this a fixed set of types or a combinable set of options? Fixed types = inheritance. Combinable options = decorator. This one question determines the right pattern.

Stacking is where the decorator pattern shows its full power. Each decorator adds one responsibility, and multiple decorators combine into a processing pipeline. The most famous real-world example is Java's I/O stream library.

Stacking Decorators on IO Streams

Java I/O Streams

Reading a file with buffering in Java looks like this:

java
InputStream stream = new BufferedInputStream(
    new FileInputStream("data.txt")
);

FileInputStream reads raw bytes from disk. BufferedInputStream wraps it, adding a memory buffer that reduces the number of expensive disk reads. Both implement InputStream. The client calls stream.read() without knowing whether it is buffered or not.

Need encryption and compression too?

java
1InputStream stream = new GZIPInputStream(
2    new CipherInputStream(
3        new BufferedInputStream(
4            new FileInputStream("data.txt")
5        ),
6        cipher
7    )
8);

Four layers, each adding one behavior. The data flows through all of them on every read: disk, buffer, decrypt, decompress. The client sees a plain InputStream.

Order Matters

The stacking order defines the processing pipeline. For writing:

 
1Application data
2Compress (smaller payload)
3Encrypt (secure the compressed data)
4Buffer (batch writes to disk)
5Write to file

Reversing compress and encrypt produces different results. Compressing encrypted data is nearly useless because encrypted data has no patterns for the compression algorithm to exploit. The correct order is compress first, then encrypt.

Practical Guidelines

When stacking decorators, follow these principles:

  1. Innermost = closest to the data source. File streams, network sockets, and database connections go at the center.
  2. Transformation decorators go in the middle. Compression, encryption, and formatting modify the data as it flows.
  3. Observation decorators go on the outside. Logging, metrics, and monitoring wrap everything so they see the final form.
  4. Performance decorators (buffering) go near the I/O boundary. Buffering is most effective close to the slow resource (disk, network).

The decorator pattern is not just a textbook concept: it is one of the most widely used patterns in production software. Once you know what to look for, you see it everywhere.

Decorator vs Inheritance Side by Side

Web Middleware

Every modern web framework uses decorators for middleware. In Express.js, Django, Flask, and Spring, each middleware function wraps the next handler:

python
1# Python/Flask-style middleware
2@app.before_request
3def log_request():
4    print(f"Request: {request.method} {request.path}")
5
6@app.before_request
7def authenticate():
8    if not valid_token(request.headers.get("Authorization")):
9        return jsonify(error="Unauthorized"), 401

Each middleware is a decorator on the request pipeline. Authentication wraps logging, which wraps the application handler. A request flows through each layer: if any layer rejects it (like auth returning 401), the chain stops. This is exactly the decorator pattern applied to HTTP processing.

Python's @decorator Syntax

Python has first-class support for decorators with the @ syntax:

python
1def log_calls(func):
2    def wrapper(*args, **kwargs):
3        print(f"Calling {func.__name__}")
4        result = func(*args, **kwargs)
5        print(f"Returned {result}")
6        return result
7    return wrapper
8
9@log_calls
10def add(a, b):
11    return a + b

@log_calls wraps the add function with logging behavior. The underlying mechanism is identical to the OOP decorator pattern: add is replaced by wrapper, which delegates to the original add after adding behavior.

Where Else You See It

  • React higher-order components, withAuth(withLogging(MyComponent)) wraps a React component with authentication and logging
  • Database connection pooling, A logging wrapper around a connection pool that records query times
  • Caching layers, A cache decorator checks the cache before delegating to the real data source
  • API rate limiting, A rate-limit decorator wraps an API client, tracking call counts before delegating
Interview Tip

In interviews, if the problem involves optional cross-cutting concerns (logging, caching, authentication, rate limiting) that should be composable, the Decorator pattern is almost certainly the right answer. These concerns are independent, stackable, and should not pollute the core business logic.

Practice Problems

Put the Decorator pattern into practice. Start with the coffee shop to build the core wrapping mechanic, then implement text formatting decorators for a different domain, and finish with the I/O streams challenge which tests your understanding of stacking order and bidirectional decoration.

Loading problem...

Loading problem...

Loading problem...