Senior 4 min · March 17, 2026

Decorator Pattern in Java — Avoiding Stream Type Mismatch

Mixing InputStream with Reader decorators outputs binary garbage.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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.

DecoratorDemo.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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package io.thecodeforge.java.patterns;

// Step 1: Define the component interface
interface TextProcessor {
    String process(String text);
}

// Step 2: Concrete component
class PlainTextProcessor implements TextProcessor {
    @Override
    public String process(String text) {
        return text;
    }
}

// Step 3: Abstract decorator — holds a reference to the wrapped component
abstract class TextProcessorDecorator implements TextProcessor {
    protected final TextProcessor wrapped;
    TextProcessorDecorator(TextProcessor wrapped) { this.wrapped = wrapped; }
}

// Step 4: Concrete decorators
class UpperCaseDecorator extends TextProcessorDecorator {
    UpperCaseDecorator(TextProcessor wrapped) { super(wrapped); }
    @Override public String process(String text) {
        return wrapped.process(text).toUpperCase(); // add before/after
    }
}

class TrimDecorator extends TextProcessorDecorator {
    TrimDecorator(TextProcessor wrapped) { super(wrapped); }
    @Override public String process(String text) {
        return wrapped.process(text.trim());  // modify input
    }
}

class PrefixDecorator extends TextProcessorDecorator {
    private final String prefix;
    PrefixDecorator(TextProcessor wrapped, String prefix) {
        super(wrapped);
        this.prefix = prefix;
    }
    @Override public String process(String text) {
        return prefix + wrapped.process(text);
    }
}

public class DecoratorDemo {
    public static void main(String[] args) {
        // Compose freely at runtime
        TextProcessor simple = new PlainTextProcessor();
        TextProcessor trimmed = new TrimDecorator(simple);
        TextProcessor upper   = new UpperCaseDecorator(trimmed);
        TextProcessor tagged  = new PrefixDecorator(upper, "[RESULT] ");

        System.out.println(tagged.process("  hello world  "));
        // [RESULT] HELLO WORLD
    }
}
The Onion Layering Mental Model
  • 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.
Key Takeaway
Interface + abstract wrapper + concrete decorators = runtime composable behaviour.
Wrap order determines behaviour sequence.
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;

public class IODecorators {
    public static void main(String[] args) throws Exception {
        // 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 Reader

        try (BufferedReader reader = new BufferedReader(
                new FileReader("data.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }

        // Stack more decorators for more behaviour:
        // FileInputStream → GZIPInputStream → InputStreamReader → BufferedReader
        try (BufferedReader gzipReader = new BufferedReader(
                new InputStreamReader(
                    new GZIPInputStream(
                        new FileInputStream("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.

DecoratorVsInheritance.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
// Inheritance approach — explosion of subclasses
class BufferedFileReader extends FileReader { ... }
class BufferedGzipFileReader extends FileReader { ... }
class BufferedEncryptedFileReader extends FileReader { ... }
// ...8 classes total

// Decorator approach — 3 wrappers
BufferedReader reader = new BufferedReader(
    new EncryptionDecorator(
        new GzipDecompressor(
            new FileReader("file.gz.enc"))));
The 2^n Rule
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.

MistakeExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WRONG: decorator doesn't delegate — chain broken
class BrokenDecorator extends TextProcessorDecorator {
    @Override
    public String process(String text) {
        // BUG: forgot to call wrapped.process()
        return text.toUpperCase();  // PlainTextProcessor's behaviour lost
    }
}

// CORRECT:
class CorrectDecorator extends TextProcessorDecorator {
    @Override
    public String process(String text) {
        return wrapped.process(text).toUpperCase();
    }
}
The Missing Delegation Bug
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. Follows Open/Closed Principle| 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               | Adds new behaviour        | Controls access           | Converts interface        |
+----------------------+---------------------------+---------------------------+---------------------------+
| Interface            | Same as wrapped           | Same as wrapped           | Different from wrapped    |
+----------------------+---------------------------+---------------------------+---------------------------+
| Example (TextProc.)  | UpperCaseDecorator wraps  | LazyInitProxy wraps       | XmlToTextAdapter wraps    |
|                      | PlainTextProcessor.       | PlainTextProcessor.       | XmlProcessor.             |
+----------------------+---------------------------+---------------------------+---------------------------+
| Behaviour Change     | Extends (adds features)   | May restrict or defer     | No behaviour change,      |
|                      |                           |                           | only interface translation|
+----------------------+---------------------------+---------------------------+---------------------------+
| JDK Example          | BufferedReader            | Collections.unmodifiable  | InputStreamReader         |
|                      |                           | List                      | (InputStreamReader)     |
+----------------------+---------------------------+---------------------------+---------------------------+
| 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 behaviour
class LazyProxy implements TextProcessor {
    private TextProcessor real;
    @Override
    public String process(String text) {
        if (real == null) real = new PlainTextProcessor();
        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.
Key Takeaway
Decorator adds behaviour. Proxy controls access. Adapter converts interface.
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 UpperCaseDecorator
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class UpperCaseDecoratorTest {
    @Test
    void shouldUppercaseOutput() {
        TextProcessor mock = text -> "hello";  // mock
        UpperCaseDecorator decorator = new UpperCaseDecorator(mock);
        assertEquals("HELLO", decorator.process(""));
    }
}

// Integration test: chain order matters
@Test
void trimThenUppercaseChain() {
    TextProcessor chain = new UpperCaseDecorator(
                             new TrimDecorator(
                                 new PlainTextProcessor()));
    assertEquals("HELLO", chain.process("  hello  "));
}

@Test
void uppcaseThenTrimChain() {
    TextProcessor chain = new TrimDecorator(
                             new UpperCaseDecorator(
                                 new PlainTextProcessor()));
    // Different result! UpperCaseDecorator sees spaces before trim
    assertEquals("  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
AspectDecoratorProxyAdapter
IntentAdds new behaviourControls accessConverts interface
InterfaceSame as wrapped objectSame as wrapped objectDifferent from wrapped object
Behaviour changeExtends (more functionality)May restrict or deferNo behaviour change, only interface translation
Example in JDKBufferedReaderCollections.unmodifiableListInputStreamReader
Number of wrappeesExactly oneExactly oneExactly one
TransparencyCaller may not know decorators existCaller 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Decorator and Inheritance?
02
What is the difference between Decorator and Proxy?
03
Can I nest decorators indefinitely?
04
How does the Decorator pattern relate to the Open/Closed Principle?
05
Can a concrete decorator be used as a component for another decorator?
🔥

That's Advanced Java. Mark it forged?

4 min read · try the examples if you haven't

Previous
Strategy Pattern in Java
17 / 28 · Advanced Java
Next
Dependency Injection in Java