The Decorator pattern adds behaviour at runtime by wrapping an object of the same interface
Core components: Component interface, ConcreteComponent, Decorator base class, ConcreteDecorators
You compose decorators like layers, each adds a piece of behaviour before/after delegation
Java I/O streams (BufferedReader wrapping FileReader) are the canonical production use case
Performance cost: each decorator adds a method call overhead, but rarely matters in I/O-bound code
Production gotcha: forget to pass the wrapped object to the next decorator → behaviour chain breaks silently
The Decorator pattern solves the problem that inheritance cannot — adding combinations of behaviour at runtime. If you need a text encoder that can be optionally buffered, optionally compressed, and optionally encrypted, inheritance gives you 2³ = 8 subclasses. The Decorator pattern gives you three wrappers that you compose freely.
You use this pattern constantly in Java without realising it. Every time you write new BufferedReader(new FileReader(path)), you are using the Decorator pattern.
Building a Decorator — Step by Step
Let's build it from scratch. The core idea: you define a common interface, a base implementation, then an abstract decorator that holds a reference to another instance of the same interface. Each concrete decorator overrides the interface method, does something extra, and delegates to the wrapped object.
Here we build a TextProcessor that can be decorated with uppercasing, trimming, and prefixing. Notice how each decorator adds its behaviour either before or after the delegation call.
The outermost decorator receives the original call first.
It can pre-process the input, then delegates to the next layer.
The innermost (concrete component) does the real work.
On the way back, each layer post-processes the result.
The order of wrapping determines the order of behaviour application.
Production Insight
The order of wrapping matters. In the example, TrimDecorator runs before UpperCaseDecorator. If you reverse them, trimming happens after uppercasing, which may not matter here, but for things like encryption-then-compression order is critical.
Never make a decorator's behaviour depend on a specific wrapping order unless documented. Test both orders in production to avoid surprises.
Rule: Keep decorator behaviour commutative (order-independent) or document the required order clearly.
Each decorator must delegate to the wrapped object or the chain breaks.
When to Use a Decorator vs a Simple Subclass
IfOne fixed extra behaviour, no variations at runtime
→
UseUse inheritance — a simple subclass is cleaner.
IfMultiple optional behaviours that can be combined freely
→
UseUse Decorator — avoids combinatorial class explosion.
IfBehaviour needs to be added/removed at runtime
→
UseUse Decorator — with delegation you can swap or remove wrappers dynamically.
IfYou want to change the behaviour of a whole class of objects (e.g. all threads)
→
UseInheritance won't work; Decorator on a per-instance basis is the way.
UML Structure Diagram
The class diagram below shows the canonical Decorator pattern structure used in Java. The Component interface defines the contract. ConcreteComponent provides the base implementation. The abstract Decorator class holds a reference to a Component and implements the same interface. ConcreteDecorators extend Decorator and add specific behaviour before or after delegation.
Roles in the Diagram
PlainTextProcessor is the ConcreteComponent. UpperCaseDecorator, TrimDecorator, PrefixDecorator are ConcreteDecorators. All implement the same interface, enabling recursive composition.
Production Insight
In production UML diagrams, always include the abstract decorator with the wrapped reference. This clarifies that every concrete decorator must accept a component in its constructor. When reviewing code, verify that the decorator hierarchy matches this structure — missing the abstract wrapper often leads to duplicate delegation code.
Key Takeaway
The abstract Decorator class with a component reference is the structural heart of the pattern. Without it, each concrete decorator would need its own wrapper field, duplicating code and breaking the pattern's intent.
Java I/O — The Real-World Decorator Example
The Java I/O library is the textbook example of the Decorator pattern. Every stream class (InputStream, OutputStream, Reader, Writer) is a component. Abstract decorators like FilterInputStream and FilterReader wrap another stream and add behaviour.
BufferedReader wraps a Reader and adds buffering. GZIPInputStream wraps an InputStream and adds decompression. You can stack them arbitrarily because they all share the same abstract parent.
IODecorators.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.thecodeforge.java.patterns;
import java.io.*;
import java.util.zip.GZIPInputStream;
publicclassIODecorators {
publicstaticvoidmain(String[] args) throwsException {
// Each wrapper adds behaviour without modifying the inner class:// FileReader: reads characters from a file// BufferedReader: adds buffering (reads in chunks, not char by char)// — both implement Readertry (BufferedReader reader = newBufferedReader(
newFileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// Stack more decorators for more behaviour:// FileInputStream → GZIPInputStream → InputStreamReader → BufferedReadertry (BufferedReader gzipReader = newBufferedReader(
newInputStreamReader(
newGZIPInputStream(
newFileInputStream("data.gz"))))) {
System.out.println(gzipReader.readLine());
}
// No class was modified — behaviour added by wrapping
}
}
Watch the Type Families
Java I/O has two separate hierarchies: byte streams (InputStream/OutputStream) and character streams (Reader/Writer). You cannot mix them directly. To go from bytes to characters, use InputStreamReader (which itself is a decorator). Forget this and you'll get confusing binary output.
Production Insight
The biggest production bug with Java I/O decorators is premature closure. When you close the outermost decorator (e.g., BufferedReader), it closes all underlying streams in the chain. If you also close the inner FileReader manually, you get an IOException: Stream closed.
Only close the outermost decorator. Use try-with-resources on the outermost one and never close individual decoraotrs.
Rule: Never close a wrapped stream directly. Close only the top-level decorator.
Key Takeaway
Java I/O streams are decorators — stack them freely.
Use InputStreamReader to bridge byte → character.
Never close inner streams. Only close outermost wrapper.
Java I/O Class Hierarchy Diagram
The diagram below shows the two main stream families in Java I/O: the byte-oriented InputStream hierarchy (left) and the character-oriented Reader hierarchy (right). FilterInputStream and FilterReader are the abstract decorators. Concrete decorators like BufferedInputStream, GZIPInputStream, BufferedReader, and InputStreamReader extend them. Note that InputStreamReader is a special decorator that adapts an InputStream to a Reader, acting as a bridge between the two families.
Bridge Across Families
InputStreamReader is the only standard decorator that crosses families: it wraps an InputStream but extends Reader. Use it whenever you need to convert byte input to character input.
Production Insight
Knowing this hierarchy prevents the most common I/O bug: wrapping an InputStream with a Reader-only decorator like BufferedReader. Always insert InputStreamReader between byte and character layers. In production monitoring, if you see garbled output or exceptions about incompatible types, this is the first thing to check.
Key Takeaway
Java I/O has two parallel decorator trees: InputStream and Reader. Use InputStreamReader as the adapter between them. Never assume a byte stream decorator produces characters.
When to Choose Decorator Over Inheritance
Inheritance is compile-time, Decorator is runtime. If you know at code-writing time exactly what behaviour an object needs, inheritance is simpler. But if behaviours are optional and combinatorial, inheritance forces you to create a class for every combination.
Example: a text processor that can optionally log, optionally encrypt, and optionally compress. With inheritance, you'd need 2^3 = 8 classes. With Decorator, you write 3 concrete decorators and compose them at object creation.
The trade-off: Decorator adds indirection and slightly more memory (each wrapper is an object). But in most applications, the flexibility far outweighs the overhead.
If you have N optional behaviours, inheritance needs 2^N subclasses. Decorator needs N decorators.
Production Insight
Inheritance also makes unit testing harder. If you need to test a buffered, compressed reader, you must instantiate the subclass that combines both. With Decorator, you test each decorator in isolation and trust the composition.
Decorator wins when the behaviours are truly orthogonal — they don't interfere with each other. If behaviours interact (e.g., encryption changes compression ratio), inheritance might force you to handle that by overriding methods, but with Decorator you can embed that logic in the wrapper.
Rule: When you hear "I need a custom subclass for this combination", reconsider — a decorator chain may already exist.
Key Takeaway
Inheritance is for fixed, compile-time behaviour extensions.
Decorator is for runtime composable, optional behaviour layers.
If you have more than 2 independent optional behaviours, default to Decorator.
Common Mistakes with the Decorator Pattern
Decorators seem simple but there are traps. The most common: forgetting to delegate to the wrapped object. If your decorator overrides a method but doesn't call wrapped.method(), the entire chain below is bypassed. This is a silent failure — behaviour just disappears.
Another: wrapping the wrong type. In Java I/O, wrapping an InputStream with a Reader directly won't compile. You need the adapter decorator InputStreamReader.
Third: breaking the contract. A decorator should not change the semantic behaviour of the component — only add to it. If your decorator changes return values in a way that violates the original contract, you've created a bug rather than a feature.
If a decorator doesn't call wrapped.method(), the chain is broken. All decorators below it become inactive. Debug by adding logging or a unit test that validates the chain produces expected composite behaviour.
Production Insight
In production, the missing delegation bug is hard to catch because the code compiles and runs, just produces wrong output. It's a logic error, not a crash.
Best defence: write a unit test that wraps a component with a decorator and asserts the final output includes both the component's base behaviour and the decorator's added behaviour. If both are present, delegation works.
Rule: Every concrete decorator method must call wrapped.method() at some point — either before, after, or around.
Key Takeaway
Always delegates to the wrapped component.
Don't change the contract — only enhance behaviour.
Write integration tests for decorator chains.
Advantages and Disadvantages of the Decorator Pattern
The Decorator pattern is powerful but not free. Understanding its pros and cons helps you decide when to use it. Below is a summary of the key trade-offs.
AdvantagesVsDisadvantages.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+--------------------------------+--------------------------------+
| Advantages | Disadvantages |
+--------------------------------+--------------------------------+
| 1. Runtime behaviour composi- | 1. Many small objects |
| tion without modifying | (increased memory & GC) |
| existing code. | |
+--------------------------------+--------------------------------+
| 2. Avoids subclass explosion | 2. Complex chains can be |
| (2^N problem). | hard to debug |
+--------------------------------+--------------------------------+
| 3. FollowsOpen/ClosedPrinciple| 3. Order dependency can lead |
| - classes open for extension,| to subtle bugs |
| closed for modification. | |
+--------------------------------+--------------------------------+
| 4. Each decorator is loosely | 4. Instantiation overhead |
| coupled and independently | per decorated object |
| testable. | |
+--------------------------------+--------------------------------+
| 5. Supports single responsibility|5. Extra indirection may affect|
| - each decorator handles one | readability for simple cases|
| concern. | |
+--------------------------------+--------------------------------+
When to Avoid Decorator
If you only have one or two fixed behaviours, inheritance is simpler. Also avoid deep nesting (>5) in performance-critical loops. For configuration-driven behaviour, consider Strategy pattern as an alternative.
Production Insight
In production, the object explosion is the biggest hidden cost. Each decorator is an object with its own small memory footprint. If you create thousands of decorator chains per second (e.g., in a high-throughput proxy), the GC pressure can become significant. Mitigate by reusing chains or using flyweight patterns for stateless decorators.
Also, deep chains can make stack traces confusing. Include a custom toString() in each decorator to aid debugging.
Key Takeaway
Decorator offers flexibility at the cost of object count and indirection. Weigh these when designing. For most I/O-bound Java applications, the advantages far outweigh the disadvantages.
Decorator vs Proxy vs Adapter: Side-by-Side Comparison
All three patterns involve wrapping an object, but their intents differ. The table below highlights the key differences using our TextProcessor interface as a common example.
PatternComparisonTable.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+----------------------+---------------------------+---------------------------+---------------------------+
| Aspect | Decorator | Proxy | Adapter |
+----------------------+---------------------------+---------------------------+---------------------------+
| Intent | Addsnew behaviour | Controls access | Convertsinterface |
+----------------------+---------------------------+---------------------------+---------------------------+
| Interface | Same as wrapped | Same as wrapped | Different from wrapped |
+----------------------+---------------------------+---------------------------+---------------------------+
| Example (TextProc.) | UpperCaseDecorator wraps | LazyInitProxy wraps | XmlToTextAdapter wraps |
| | PlainTextProcessor. | PlainTextProcessor. | XmlProcessor. |
+----------------------+---------------------------+---------------------------+---------------------------+
| BehaviourChange | Extends (adds features) | May restrict or defer | No behaviour change, |
| | | | only interface translation|
+----------------------+---------------------------+---------------------------+---------------------------+
| JDKExample | BufferedReader | Collections.unmodifiable | InputStreamReader |
| | | List | (InputStream→Reader) |
+----------------------+---------------------------+---------------------------+---------------------------+
| Transparency | Caller may not know | Caller may be blocked | Caller expects the |
| | decorators exist | (exception thrown) | adapted interface |
+----------------------+---------------------------+---------------------------+---------------------------+
Name Your Wrappers Clearly
Use naming conventions: XxxDecorator, XxxProxy, XxxAdapter. The class name immediately communicates intent to other developers.
Production Insight
Misidentifying a Proxy as a Decorator can lead to security gaps. For example, an access-control Proxy that throws an exception if the user isn't authenticated might be used as a Decorator, inadvertently exposing sensitive operations. Always document the wrapper's intent in the class Javadoc.
When reviewing code, check: does this wrapper add behaviour (Decorator), control access (Proxy), or convert types (Adapter)?
Key Takeaway
Same structural trick (wrapping), different semantic goal. Use names to signal intent. Never use a Proxy where a Decorator is expected, and vice versa.
Performance Implications of Decorator Chains
Each decorator adds a method call and an object allocation. In most Java applications, especially I/O-bound ones, this overhead is negligible. But high-frequency calls (e.g., processing millions of log lines per second) can suffer.
The method call chain is the primary cost: each decorator adds one virtual method call. For a stack of 3 decorators, a single operation goes through 4 calls (3 decorators + the concrete component). This is rarely a bottleneck — modern JVMs can inline short call chains.
Memory: each decorator is an object. If you create thousands of decorator chains per request (e.g., per-stream for each HTTP request), the allocation pressure can stress the garbage collector. Consider pooling or caching decorator chains where possible.
PerformanceTest.javaJAVA
1
2
3
4
5
6
7
8
// Rough benchmark of decorator overhead (nanoseconds per operation)// Measured on a typical JVM with C2 compilation// Plain component: ~2 ns// 1 decorator: ~5 ns// 3 decorators: ~12 ns// 5 decorators: ~20 ns// These numbers include JIT warmup.// In I/O-bound code, the disk/network latency (>1ms) far outweighs this overhead.
When Overhead Matters
Worry about decorator overhead only when the decorated method is called millions of times per second and is already CPU-bound. For typical I/O, it's invisible.
Production Insight
The real performance pitfall is not the decorator calls but the creation cost. For example, wrapping a new FileInputStream for every HTTP request and then wrapping it in decorators creates allocation pressure. Use connection pools or object pools to reuse streams.
Also, be aware that try-with-resources creates a hidden compiler-generated close chain. That's cheap.
Rule: Profile before optimising. Decorator overhead is rarely the culprit. If it is, consider removing layers that don't provide value, or use static decoration (compile-time weaving) if absolute max perfoance is needed.
Key Takeaway
Decorator overhead is negligible in I/O-bound code.
Object creation cost matters more than method call overhead.
Measure before you optimise — don't break composability for imaginary gains.
Decorator vs Other Structural Patterns
The Decorator pattern is often confused with Proxy and Adapter. All three involve wrapping, but the intent differs:
Decorator: Adds new behaviour (enhances).
Proxy: Controls access to the wrapped object (e.g., lazy initialisation, access control, caching). It provides the same interface but may restrict or defer.
Adapter: Converts one interface to another — the wrapped object has a different interface, and the adapter makes it compatible.
In real code, lines blur. Java's Collections.unmodifiableList is a Decorator (adds immutability behaviour) but also acts like a Proxy (prevents modification). The distinction is intent: are you adding behaviour or controlling access?
Another pattern: Composite. Composite treats a group of objects as a single object. Decorator always wraps a single object. Composite has children; Decorator has one delegate.
PatternComparison.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Proxy example: Controls access, no new behaviourclassLazyProxyimplementsTextProcessor {
privateTextProcessor real;
@OverridepublicStringprocess(String text) {
if (real == null) real = newPlainTextProcessor();
return real.process(text);
}
}
// Adapter example: Converts interface// InputStreamReader adapts InputStream to Reader – different interface// Composite example: Not applicable here – composite deals with aggregation, not single wrapper
Same Structure, Different Intent
Decorator: adds behaviour — you get more than before.
Proxy: controls access — you may get less (or the same with checks).
Adapter: changes interface — the client sees a different contract.
In Java's I/O, FilterInputStream is a Decorator by name, but some subclasses (like BufferedInputStream) are Decorator, while others (like CipherInputStream) are both Decorator and Proxy (controls access via encryption).
Production Insight
Confusing Proxy and Decorator can lead to security holes. If you meant to add caching (Decorator) but wrote a Proxy that skips validation, you've weakened access control. Document the intent in the class comment.
When reviewing code, ask: "Is this wrapper adding behaviour or restricting access?" If the wrapper modifies the behaviour without the caller's knowledge, it's a Decorator. If it may refuse to perform the operation, it's a Proxy.
Rule: Use the class name to signal intent: XxxDecorator, XxxProxy, XxxAdapter.
Same structural trick, different semantic outcome.
Name your wrappers clearly to communicate intent.
Testing Decorator Chains
Testing decorators requires two kinds of tests. First, unit test each concrete decorator in isolation by wrapping a mock component and verifying the transformed output. Second, integration test the chain to ensure all decorators work together.
The danger of unit-testing only individual decorators is that you miss interactions — a decorator might rely on the exact formatting of the wrapped object's output, which changes when another decorator is inserted in the middle.
Use dependency injection to make decorators testable: pass the wrapped object through the constructor, which allows mocking.
TestDecorator.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Unit test for UpperCaseDecoratorimportstatic org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
classUpperCaseDecoratorTest {
@TestvoidshouldUppercaseOutput() {
TextProcessor mock = text -> "hello"; // mockUpperCaseDecorator decorator = newUpperCaseDecorator(mock);
assertEquals("HELLO", decorator.process(""));
}
}
// Integration test: chain order matters
@TestvoidtrimThenUppercaseChain() {
TextProcessor chain = newUpperCaseDecorator(
newTrimDecorator(
newPlainTextProcessor()));
assertEquals("HELLO", chain.process(" hello "));
}
@TestvoiduppcaseThenTrimChain() {
TextProcessor chain = newTrimDecorator(
newUpperCaseDecorator(
newPlainTextProcessor()));
// Different result! UpperCaseDecorator sees spaces before trimassertEquals(" HELLO ", chain.process(" hello "));
}
Test Chain Order Variants
Write tests for the expected wrapping order and also for reversed order to catch bugs if someone refactors the chain later. The test documents the intended order.
Production Insight
If your decorator chain is constructed by a factory or dependency injection container, the order may not be obvious. Write a test that verifies the actual chain behaviour, not just component behaviour.
Also test with null inputs and edge cases: an empty text, very large text, Unicode characters. Decorators that call .toUpperCase() without Locale can produce unexpected results for Turkish i/I.
Rule: Test the chain as a whole, not just individual decorators.
Key Takeaway
Unit test decorators in isolation with mocks.
Integration test the full chain in expected and unexpected orders.
Document and enforce wrapping order in tests.
Practice Exercises
Applying the Decorator pattern through exercises solidifies understanding. Below are five progressively challenging tasks. Each exercise provides a scenario and expected behavior. Try implementing them before checking the solution hints.
Exercises.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Exercise 1: Pizza Ordering// Create a Pizza interface with getDescription() and getCost().// Implement PlainPizza. Then create decorators: Cheese, Pepperoni, Mushroom.// Ensure stacking works: new Mushroom(new Pepperoni(new Cheese(new PlainPizza()))).// Expected: Description = "Plain Pizza, Cheese, Pepperoni, Mushroom", Cost = 8.50 (base 5 + 1.50 + 1.00 + 1.00).// Exercise 2: Logging HTTP Calls// Assume an HttpClient interface with send(Request) method.// Implement a LoggingDecorator that logs request and response timestamps.// The decorator should not modify the actual HTTP call behavior.// Exercise 3: Caching Decorator// Create a DataService interface with fetchData(String key).// Implement a CachingDecorator that caches results in a HashMap.// The decorator should delegate to the wrapped service on cache miss.// Exercise 4: Encryption Decorator for File I/O// Use the existing TextProcessor pattern. Create an EncryptionDecorator// that Base64-encodes the text before delegating and decodes after.// Test with round-trip: encrypt then decrypt should yield original.// Exercise 5: Decorator with State// Create a CountingDecorator that counts how many times process() is called.// Expose a getCount() method on the decorator (not on the interface).// This shows how decorators can add new methods, but clients must know// the concrete decorator type to access them.
Solution Hint
For Exercise 5, declare CountingDecorator as a concrete decorator with a private int count field. The process() method increments the counter before delegating. Add a public getCount() method. Note that this breaks the pure Decorator pattern because the client must cast to CountingDecorator to access the new method.
Production Insight
In production, exercises like the logging decorator are common. However, be careful with sensitive data in logs — never log request bodies that contain passwords or PII. Use a parameterized log message format that can be filtered.
The caching decorator should include a TTL (time-to-live) mechanism. Without it, stale data can cause subtle bugs. Consider using a library like Caffeine instead of rolling your own.
Key Takeaway
Practice makes perfect: implement these exercises to internalize the decorator structure. Start with simple pizza toppings, then move to real-world scenarios like logging and caching.
● Production incidentPOST-MORTEMseverity: high
Stacking Decorators Without Proper Type Matching
Symptom
Output from reading a gzip file was binary garbage — no readable characters, random symbols printed.
Assumption
GZIPInputStream produces text directly, so reading bytes with read() returns characters.
Root cause
GZIPInputStream is an InputStream (byte-oriented), not a Reader (character-oriented). The decorator chain mixed stream types, causing byte values to be printed as-is instead of decoded to characters.
Fix
Always ensure decorator types match: wrap GZIPInputStream with InputStreamReader, then BufferedReader.
Key lesson
Decorator chains must respect the abstraction interface — you cannot mix byte and character streams without conversion.
Java I/O decorators are divided into two families: InputStream/OutputStream (bytes) and Reader/Writer (characters). Stay within one family or use an adapter like InputStreamReader.
Before writing any I/O decorator chain, check the least common interface of all participants.
Production debug guideSymptom-based guide to fixing broken decorator stacks3 entries
Symptom · 01
Output is garbled binary instead of text
→
Fix
Check chain: do you have a Reader on top of an InputStream? Insert InputStreamReader to convert bytes to characters.
Symptom · 02
Unexpected IOException: Stream closed
→
Fix
Verify that the outermost decorator is the only one closed. Closing inner streams manually before outer is done causes premature closure. Use try-with-resources on the outermost decorator only.
Symptom · 03
Behaviour appears missing (e.g. no buffering, no encryption)
→
Fix
Check that the decorator’s methods actually delegate to the wrapped object. A common mistake is to forget to call super.method() or wrapped.method() causing the behaviour to be lost.
★ Quick Debug: Decorator Chain FailuresWhen your decorator chain misbehaves, these commands will identify the issue fast.
Behaviour not added (e.g. no encryption)−
Immediate action
Inspect the constructor of each decorator — does it call super(wrapped)?
Commands
Print stack trace at runtime: new Exception().printStackTrace() inside the decorator method
Enable debug logging at each decorator: System.out.println("EncryptDecorator.process() called")
Fix now
Add super.process() call inside the decorator method if missing
NullPointerException in decorator method+
Immediate action
Check that the wrapped field is not null. Likely the decorator was created with null or the wrapped object was closed earlier.
Commands
Add null check: if (wrapped == null) throw new IllegalStateException("Wrapped object is null")
Inspect the calling code to ensure the decorator is constructed after the wrapped object is initialised
Fix now
Reorder initialisation so wrapped object exists before passing to decorator
Decorator vs Related Patterns
Aspect
Decorator
Proxy
Adapter
Intent
Adds new behaviour
Controls access
Converts interface
Interface
Same as wrapped object
Same as wrapped object
Different from wrapped object
Behaviour change
Extends (more functionality)
May restrict or defer
No behaviour change, only interface translation
Example in JDK
BufferedReader
Collections.unmodifiableList
InputStreamReader
Number of wrappees
Exactly one
Exactly one
Exactly one
Transparency
Caller may not know decorators exist
Caller may be blocked (exception thrown)
Caller expects the adapted interface
Key takeaways
1
Decorator adds behaviour at runtime by wrapping an object that implements the same interface.
2
The key structure
decorator holds a reference to the wrapped object and delegates to it.
3
Decorators compose freely
wrap N decorators around one component for N behaviours.
4
Java's I/O library (BufferedReader, GZIPInputStream) is the most-used Decorator in the JDK.
5
Prefer Decorator over inheritance when you need combinations of optional behaviours.
6
Always delegate
a decorator that doesn't call wrapped.method() breaks the chain.
Common mistakes to avoid
4 patterns
×
Missing delegation to wrapped object
Symptom
The decorator's method does not call wrapped.method(), so the chain is broken. Behaviour from inner layers is lost without any error.
Fix
Ensure every overridden method in a concrete decorator calls wrapped.method() either before, after, or around its own logic.
×
Closing inner streams manually
Symptom
IOException: Stream closed when reading from the outermost decorator after an inner stream was explicitly closed.
Fix
Only close the outermost decorator. Use try-with-resources on the top-level decorator and never call close() on any wrapped stream.
×
Mixing byte and character families without adapter
Symptom
Compilation error when trying to wrap an InputStream with a Reader directly, or runtime binary output when reading characters from a GZIPInputStream without an InputStreamReader.
Fix
Insert InputStreamReader as an adapter between byte and character streams. Never skip this step.
×
Assuming decorator order doesn't matter
Symptom
Behaviour changes when wrapping order changes, e.g., encryption then compression vs compression then encryption produce different results.
Fix
Document the intended order. Write integration tests that verify the composite behaviour. Consider making decorators commutative where possible.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Explain the Decorator pattern and give a real Java example.
Q02SENIOR
What problem does the Decorator pattern solve that inheritance cannot?
Q03SENIOR
How is Java's BufferedReader an example of the Decorator pattern?
Q04SENIOR
What are the performance trade-offs of using the Decorator pattern?
Q01 of 04SENIOR
Explain the Decorator pattern and give a real Java example.
ANSWER
The Decorator pattern allows behaviour to be added to an object at runtime without affecting other objects of the same class. It wraps the original object with a set of decorator objects that share the same interface. Each decorator adds its behaviour before or after delegating to the wrapped object. The canonical Java example is the I/O library: BufferedReader wraps a Reader to add buffering; GZIPInputStream wraps an InputStream to add decompression. You can stack them: new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream("file.gz")))).
Key points:
- The component interface defines the contract.
- ConcreteComponent implements the base behaviour.
- AbstractDecorator holds a reference to a component.
- ConcreteDecorators extend the abstract decorator and add behaviour.
Design decisions: The pattern avoids subclass explosion for combinable behaviours. It's a structural pattern focusing on composition.
Q02 of 04SENIOR
What problem does the Decorator pattern solve that inheritance cannot?
ANSWER
Inheritance adds behaviour at compile time. To have different combinations of optional behaviours, you'd need a subclass for every combination (2^n problem). For example, if you have three optional features — buffering, compression, encryption — inheritance requires 8 subclasses. Decorator solves this by composing behaviour at runtime through wrapping. You write one decorator per behaviour and combine them as needed. This also allows behaviour to be added or removed dynamically (e.g., wrapping with encryption only for sensitive data).
Additional advantage: Decorator can wrap any instance of the component interface, not just the concrete class. You can wrap a decorated object again, achieving multi-layered behaviour without modifying existing code.
Q03 of 04SENIOR
How is Java's BufferedReader an example of the Decorator pattern?
ANSWER
BufferedReader is a concrete decorator in the Reader hierarchy. It extends the abstract decorator FilterReader (though in practice BufferedReader extends Reader directly, but the pattern intent holds). It wraps a Reader object (e.g., FileReader, InputStreamReader) and adds buffering behaviour. It overrides read() to read larger chunks from the underlying Reader rather than one character at a time. The key evidence: you can use BufferedReader exactly where a Reader is expected (same interface), and you can combine it with other decorators. The canonical chain: new BufferedReader(new FileReader("file.txt")) demonstrates wrapping an object to add behaviour without modifying the FileReader class.
The same pattern is used for output: BufferedWriter wraps Writer to add buffering. And for byte streams: BufferedInputStream wraps InputStream.
Q04 of 04SENIOR
What are the performance trade-offs of using the Decorator pattern?
ANSWER
Decorator adds a small overhead per call: each decorator introduces one more virtual method call and object allocation. For I/O-bound operations, this is negligible (nanoseconds vs milliseconds of disk/network latency). For CPU-bound high-frequency loops, the overhead can accumulate. For example, a chain of 5 decorators may add 20-30ns per call, which matters if iterating millions of times. Memory: each decorator is an object, increasing GC pressure if chains are created per request.
Mitigations:
- Avoid unnecessary decorators (e.g., don't buffer if already buffered).
- Use static composition via code generation if chains are fixed.
- Reuse decorator chains (e.g., pool them).
- Profile before optimising. In most real applications, the flexibility justifies the cost.
01
Explain the Decorator pattern and give a real Java example.
SENIOR
02
What problem does the Decorator pattern solve that inheritance cannot?
SENIOR
03
How is Java's BufferedReader an example of the Decorator pattern?
SENIOR
04
What are the performance trade-offs of using the Decorator pattern?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the difference between Decorator and Inheritance?
Inheritance adds behaviour at compile time — you choose the subclass when writing code. Decorator adds behaviour at runtime — you choose the wrappers when creating objects. Inheritance creates an explosion of subclasses for combinations; Decorator creates a few wrappers that combine freely.
Was this helpful?
02
What is the difference between Decorator and Proxy?
Both wrap an object of the same interface. Decorator adds new behaviour — it extends what the object does. Proxy controls access — it may restrict, delay, cache, or log access to the underlying object without fundamentally changing its behaviour. The distinction is intent.
Was this helpful?
03
Can I nest decorators indefinitely?
In theory, yes. In practice, deep nesting (more than 5-6 layers) can hurt readability and performance. There's no technical limit beyond stack depth, but each decorator adds method call overhead. Beyond 10 layers you should reconsider the design.
Was this helpful?
04
How does the Decorator pattern relate to the Open/Closed Principle?
It's a direct application: the concrete component is closed for modification (you don't change its code), but you extend its behaviour through decorators (open for extension). This is a classic way to follow the Open/Closed Principle in Java.
Was this helpful?
05
Can a concrete decorator be used as a component for another decorator?
Yes. That's the whole point. A decorator implements the same interface as the component, so it can be wrapped by another decorator. This is how layers are built.