8 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
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.
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?
2. How It Works
Step-by-step mechanics:
- Define a
Component(or ) that both the real object and decorators share. - Implement
ConcreteComponent— the base object with default behavior. - Implement
BaseDecorator(also calledWrapper) — it implementsComponentand holds a reference to anotherComponent. Delegates all calls by default. - Implement
ConcreteDecoratorsubclasses — each overrides one or more methods to add behavior before or after delegating to the wrapped component. - 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")));
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?
3. Variants & Comparisons
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| GoF Decorator (class-based) | Wraps component; implements same interface; stacks | Runtime composition; no subclass explosion; OCP-compliant | Many small wrapper classes; order matters | I/O streams, middleware pipelines |
Python @decorator syntax | Callable wraps a function/method | Syntactic sugar; stdlib support (@wraps) | Function-level only (not object-level); different from GoF | Function behavior extension, AOP-style |
| Inheritance | Subclass overrides behavior | Simple for one behavior combo | Combinatorial explosion for multiple behaviors | Fixed behavior, single inheritance axis |
| Mixins | Multiple inheritance to compose behaviors | Reusable chunks of behavior | Order-dependent (MRO); can conflict | Python, 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.
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?
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)
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?
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.
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?
6. Interview Cheat Sheet
Key sentences to demonstrate depth:
- "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."
- " solves the combinatorial subclass explosion problem: instead of
EncryptedCompressedBufferedStream, I compose three single-purpose decorators at runtime in whatever order I need." - "Python's
@decoratorsyntax 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." - "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
FileDataSourceobjects 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 implementsInputStream, so any reader that acceptsInputStreamworks with the full stack.
Connections to other concepts:
- Open/Closed Principle — Decorator 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.