Decorator Pattern

8 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Decorator Pattern

1. What Is It?

The pattern attaches additional responsibilities to an object dynamically, at runtime, without modifying the object's class or any other object in the hierarchy. Decorators provide a flexible alternative to subclassing for extending functionality: instead of creating a EncryptedCompressedBufferedFileWriter subclass, you wrap a FileWriter in a BufferedDecorator, then in a CompressionDecorator, then in an EncryptionDecorator — composing behaviors in whatever order you need.

Without , extending object behavior through leads to a combinatorial explosion of subclasses. Five independent behaviors across three base types means up to 2⁵ × 3 = 96 subclass combinations. Decorator lets you compose the same behaviors from a small number of reusable wrapper classes.


QUICK CHECK

A logging library supports 3 output targets (file, console, network) and 5 independent formatting behaviors (timestamps, color-coding, JSON formatting, log-level filtering, compression). If you implemented every combination as a separate subclass, how many subclasses would you need — and what does the Decorator pattern offer instead?

Choose one answer

2. How It Works

Step-by-step mechanics:

  1. Define a Component (or ) that both the real object and decorators share.
  2. Implement ConcreteComponent — the base object with default behavior.
  3. Implement BaseDecorator (also called Wrapper) — it implements Component and holds a reference to another Component. Delegates all calls by default.
  4. Implement ConcreteDecorator subclasses — each overrides one or more methods to add behavior before or after delegating to the wrapped component.
  5. Client code stacks decorators: new ConcreteDecoratorB(new ConcreteDecoratorA(new ConcreteComponent())).

Key invariant: Every implements the same as what it wraps. The client can't tell whether it's talking to the base component or a stack of decorators — they're all Component.

Python

from __future__ import annotations
from abc import ABC, abstractmethod
import base64
import zlib


class DataSource(ABC):
    """Component interface — shared by the concrete component and all decorators."""

    @abstractmethod
    def write_data(self, data: str) -> None: ...

    @abstractmethod
    def read_data(self) -> str: ...


class FileDataSource(DataSource):
    """ConcreteComponent — performs actual file I/O."""

    def __init__(self, filename: str) -> None:
        self._filename = filename
        self._data: str = ""

    def write_data(self, data: str) -> None:
        self._data = data
        print(f"[File] Writing {len(data)} chars to {self._filename}")

    def read_data(self) -> str:
        return self._data


class DataSourceDecorator(DataSource):
    """BaseDecorator — wraps any DataSource and delegates by default."""

    def __init__(self, wrappee: DataSource) -> None:
        self._wrappee = wrappee

    def write_data(self, data: str) -> None:
        self._wrappee.write_data(data)   # pure delegation — subclasses intercept

    def read_data(self) -> str:
        return self._wrappee.read_data()


class EncryptionDecorator(DataSourceDecorator):
    """ConcreteDecorator — base64-encodes on write, decodes on read."""

    def write_data(self, data: str) -> None:
        encrypted = base64.b64encode(data.encode()).decode()
        print(f"[Encryption] Encrypting data")
        super().write_data(encrypted)

    def read_data(self) -> str:
        data = super().read_data()
        return base64.b64decode(data.encode()).decode()


class CompressionDecorator(DataSourceDecorator):
    """ConcreteDecorator — zlib-compresses on write, decompresses on read."""

    def write_data(self, data: str) -> None:
        compressed = base64.b64encode(zlib.compress(data.encode())).decode()
        print(f"[Compression] Compressing data")
        super().write_data(compressed)

    def read_data(self) -> str:
        data = super().read_data()
        return zlib.decompress(base64.b64decode(data.encode())).decode()


# Usage — compose behaviors at runtime by stacking decorators
file_source = FileDataSource("data.bin")

# Layer: compression → encryption → file
source = EncryptionDecorator(CompressionDecorator(file_source))
source.write_data("Hello, World!")
# [Compression] Compressing data
# [Encryption] Encrypting data
# [File] Writing N chars to data.bin

recovered = source.read_data()
print(recovered)   # "Hello, World!"

# Swap order or layers at runtime — no subclassing required
only_encrypted = EncryptionDecorator(file_source)

Java

// Component interface
public interface DataSource {
    void writeData(String data);
    String readData();
}

// ConcreteComponent
public class FileDataSource implements DataSource {
    private final String filename;
    private String data = "";

    public FileDataSource(String filename) { this.filename = filename; }

    @Override public void writeData(String data) {
        this.data = data;
        System.out.printf("[File] Writing %d chars to %s%n", data.length(), filename);
    }

    @Override public String readData() { return data; }
}

// BaseDecorator — delegates everything by default
public class DataSourceDecorator implements DataSource {
    private final DataSource wrappee;

    public DataSourceDecorator(DataSource wrappee) { this.wrappee = wrappee; }

    @Override public void writeData(String data) { wrappee.writeData(data); }
    @Override public String readData() { return wrappee.readData(); }
}

// ConcreteDecorator — encryption (import java.util.Base64 required at file top)
public class EncryptionDecorator extends DataSourceDecorator {
    public EncryptionDecorator(DataSource wrappee) { super(wrappee); }

    @Override public void writeData(String data) {
        String encrypted = Base64.getEncoder().encodeToString(data.getBytes());
        System.out.println("[Encryption] Encrypting data");
        super.writeData(encrypted);
    }

    @Override public String readData() {
        String data = super.readData();
        return new String(Base64.getDecoder().decode(data));
    }
}

// ConcreteDecorator — compression (simplified)
public class CompressionDecorator extends DataSourceDecorator {
    public CompressionDecorator(DataSource wrappee) { super(wrappee); }

    @Override public void writeData(String data) {
        // In production: use java.util.zip.GZIPOutputStream
        String compressed = "[compressed:" + data + "]";
        System.out.println("[Compression] Compressing data");
        super.writeData(compressed);
    }

    @Override public String readData() {
        String data = super.readData();
        return data.replace("[compressed:", "").replace("]", "");
    }
}

// Usage — stacked at runtime
public class Client {
    public static void main(String[] args) {
        DataSource source = new EncryptionDecorator(
                                new CompressionDecorator(
                                    new FileDataSource("data.bin")));
        source.writeData("Hello, World!");
        System.out.println(source.readData());   // Hello, World!
    }
}

// Real-world Java I/O streams use exactly this structure:
InputStream stream = new BufferedInputStream(
                         new GZIPInputStream(
                             new FileInputStream("archive.gz")));

QUICK CHECK

You are building a data pipeline where messages can optionally be compressed, encrypted, both, or neither before being written to storage. You implement this using the Decorator pattern with a MessageSink interface, a FileMessageSink concrete component, and CompressionDecorator / EncryptionDecorator concrete decorators. A teammate asks: 'Why does CompressionDecorator also implement MessageSink instead of just extending a plain wrapper class?' What is the most accurate answer?

Choose one answer

3. Variants & Comparisons

ApproachHow It WorksProsConsBest For
GoF Decorator (class-based)Wraps component; implements same interface; stacksRuntime composition; no subclass explosion; OCP-compliantMany small wrapper classes; order mattersI/O streams, middleware pipelines
Python @decorator syntaxCallable wraps a function/methodSyntactic sugar; stdlib support (@wraps)Function-level only (not object-level); different from GoFFunction behavior extension, AOP-style
InheritanceSubclass overrides behaviorSimple for one behavior comboCombinatorial explosion for multiple behaviorsFixed behavior, single inheritance axis
MixinsMultiple inheritance to compose behaviorsReusable chunks of behaviorOrder-dependent (MRO); can conflictPython, careful use

vs. :

  • keeps the same and adds behavior.
  • provides a different than the wrapped object.

Decorator vs. Proxy:

  • Proxy controls access to the subject (lazy init, access control, caching) but doesn't add functional behavior from the outside.
  • adds functional responsibilities.

QUICK CHECK

Your team is building an HTTP middleware pipeline where each middleware component (logging, authentication, rate-limiting) must be stackable at runtime and each component must expose the same interface as the raw request handler. A teammate suggests using inheritance to create subclasses for each combination of middlewares. What is the primary drawback of the inheritance approach compared to the Decorator pattern for this use case?

Choose one answer

4. When to Use It (and When NOT To)

Use when:

  • You need to add responsibilities to individual objects at runtime, not to every object of a class.
  • Extension by subclassing is impractical due to a combinatorial explosion of possible feature combinations.
  • You want behaviors to be composable in different orders or combinations.

Do NOT use when:

  • You need to change the of the component — use .
  • The order of decoration doesn't matter and you just want all decorators always applied — a plain subclass is simpler.
  • The decorator stack becomes very deep — performance overhead and debugging complexity grow with each layer.

Anti-patterns:

  • Order dependency without documentation — if CompressionDecorator must wrap before EncryptionDecorator (or vice versa), clients may get it wrong. Document required ordering explicitly.
  • Stateful decorators modifying the wrapped object — decorators should be transparent to the client. Side-effecting the wrappee breaks the contract.

Decision triggers:

  • "I need to add behaviors to individual objects without affecting others of the same class" → Decorator
  • "I have N features that can combine in any combination with M base types" → Decorator avoids N×M subclasses
  • "I need logging, caching, or auth checks transparently around an object" → Decorator (or Proxy if access-control focused)

QUICK CHECK

A team is building a file storage service that supports optional features — compression, encryption, and virus scanning — that users can enable in any combination. Currently they're considering creating a separate subclass for every possible combination (e.g., CompressedFile, EncryptedFile, CompressedEncryptedFile, ScannedEncryptedFile, etc.). Why is the Decorator pattern a better fit here than subclassing?

Choose one answer

5. Real-World Usage

Java I/O streams BufferedInputStream, DataInputStream, GZIPInputStream, CipherInputStream all decorate InputStream. You compose them: new BufferedInputStream(new GZIPInputStream(new FileInputStream("file.gz"))) — each layer adds one behavior (buffering, decompression, file access) and passes calls through. This is the canonical GoF example in production code.

Java Collections.unmodifiableList() / synchronizedList() These static methods wrap any List in a that either throws on mutations or synchronizes all access. The underlying list is unchanged; the wrapper adds behavior transparently. The client still sees a List — same , new behavior.

Django/Flask view decorators Django's @login_required and @permission_required are Python decorators applied to view functions. They wrap the view in a function that checks authentication/authorization first, then delegates to the original view — the GoF Decorator pattern expressed as Python function wrapping. Flask's @app.route similarly wraps a view function to register it with the routing system.


QUICK CHECK

You need to add thread-safe access to an existing ArrayList that is already being used throughout your codebase via the List interface. You call Collections.synchronizedList(myList) and use the returned value. Which statement best describes what happens to the original list and how clients interact with the wrapper?

Choose one answer

6. Interview Cheat Sheet

Key sentences to demonstrate depth:

  1. "The key structural invariant is that every implements the same as what it wraps — the client can't distinguish a raw component from a decorated one, which enables transparent stacking."
  2. " solves the combinatorial subclass explosion problem: instead of EncryptedCompressedBufferedStream, I compose three single-purpose decorators at runtime in whatever order I need."
  3. "Python's @decorator syntax is inspired by this pattern but operates on functions, not objects — it's the GoF intent applied to callables via closures rather than class wrapping."
  4. "The difference between Decorator and Proxy is intent: Decorator adds functional behavior; Proxy controls access (caching, lazy initialization, authentication) while keeping the identical."

Common follow-up questions:

  • "How is Decorator different from ?" extends at compile time and applies to all instances of the subclass. Decorator extends individual objects at runtime — you can have two FileDataSource objects where one is encrypted and the other isn't.
  • "Does the order of decorator wrapping matter?" → Yes, and it's the client's responsibility. Compressing before encrypting vs. encrypting before compressing produce different bytes. The pattern doesn't enforce order — document the required sequence in the API.
  • "How do you use Decorator in Java streams?"new BufferedInputStream(new GZIPInputStream(new FileInputStream("file.gz"))) — each layer wraps the next, each implements InputStream, so any reader that accepts InputStream works with the full stack.

Connections to other concepts:

  • Open/Closed PrincipleDecorator is a direct application: the ConcreteComponent is closed for modification, but behavior is open for extension via wrapping.
  • pattern — both use recursive structure (components containing components), but treats a group of objects as a single object; Decorator adds behavior to a single object.
  • pattern swaps algorithms inside an object; Decorator wraps an object from the outside. Use Strategy when the algorithm varies; use Decorator when responsibilities stack.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.