Decorator Pattern
What the Decorator Pattern actually is
The Decorator pattern is a structural pattern that lets you add responsibilities to an object dynamically without subclassing and without changing the object's interface. You wrap the original object in another object that implements the same interface, forwards calls to the wrapped object, and adds behavior before or after the forward. Because every wrapper shares the same interface, the client cannot tell whether it is talking to a plain object or a stack of decorators.
The mechanism is recursive composition: a decorator holds a reference to a Component, and because a decorator is itself a Component, it can wrap another decorator. The result is a linked list of behavior that the client addresses through one stable type.
The problem it removes
Suppose you are building a GUI toolkit and a window can have a border, a scrollbar, or a theme. Without Decorator, every combination becomes its own subclass: WindowWithBorder, WindowWithScrollbar, WindowWithBorderAndScrollbar, and so on. The number of classes grows combinatorially, and behavior that should be independent is welded together.
Decorator solves this by making each feature a separate wrapper. A border decorator knows only about borders; a scrollbar decorator knows only about scrollbars. You compose them at runtime by wrapping one around another, and the client still sees a single Window.
The four roles
- Component — the common interface implemented by the original object and every decorator. It is the only type the client needs to know.
- Concrete Component — the plain object to which responsibilities are added (for example,
PlainPizzaorFileInputStream). - Decorator — an abstract class or interface that implements
Componentand stores a reference to anotherComponent. It usually forwards every method unchanged so concrete decorators can override only what they care about. - Concrete Decorators — classes that extend the abstract decorator and add specific responsibilities (extra cost, buffering, compression, encryption).
Worked example: Pizza toppings
A pizza has a base cost and a description. Toppings add cost and append their name to the description. Each topping is a decorator that forwards the base calls and adds its own contribution.
interface Pizza {
double getCost();
String getDescription();
}
class PlainPizza implements Pizza {
public double getCost() { return 10.0; }
public String getDescription() { return "Plain Pizza"; }
}
abstract class ToppingDecorator implements Pizza {
protected Pizza pizza;
public ToppingDecorator(Pizza pizza) {
this.pizza = pizza;
}
public double getCost() { return pizza.getCost(); }
public String getDescription() { return pizza.getDescription(); }
}
class CheeseDecorator extends ToppingDecorator {
public CheeseDecorator(Pizza pizza) { super(pizza); }
public double getCost() { return pizza.getCost() + 2.5; }
public String getDescription() { return pizza.getDescription() + ", Cheese"; }
}
class PepperoniDecorator extends ToppingDecorator {
public PepperoniDecorator(Pizza pizza) { super(pizza); }
public double getCost() { return pizza.getCost() + 3.0; }
public String getDescription() { return pizza.getDescription() + ", Pepperoni"; }
}
public class Solution {
public static void main(String[] args) {
Pizza pizza = new PepperoniDecorator(new CheeseDecorator(new PlainPizza()));
System.out.println(pizza.getCost()); // 15.5
System.out.println(pizza.getDescription()); // Plain Pizza, Cheese, Pepperoni
}
}
package main
import "fmt"
type Pizza interface {
Cost() float64
Description() string
}
type PlainPizza struct{}
func (p PlainPizza) Cost() float64 { return 10.0 }
func (p PlainPizza) Description() string { return "Plain Pizza" }
type ToppingDecorator struct {
pizza Pizza
}
func (t ToppingDecorator) Cost() float64 { return t.pizza.Cost() }
func (t ToppingDecorator) Description() string { return t.pizza.Description() }
type Cheese struct { ToppingDecorator }
func NewCheese(p Pizza) *Cheese {
return &Cheese{ToppingDecorator{pizza: p}}
}
func (c *Cheese) Cost() float64 { return c.pizza.Cost() + 2.5 }
func (c *Cheese) Description() string { return c.pizza.Description() + ", Cheese" }
type Pepperoni struct { ToppingDecorator }
func NewPepperoni(p Pizza) *Pepperoni {
return &Pepperoni{ToppingDecorator{pizza: p}}
}
func (p *Pepperoni) Cost() float64 { return p.pizza.Cost() + 3.0 }
func (p *Pepperoni) Description() string { return p.pizza.Description() + ", Pepperoni" }
func main() {
var pizza Pizza = NewPepperoni(NewCheese(PlainPizza{}))
fmt.Println(pizza.Cost()) // 15.5
fmt.Println(pizza.Description()) // Plain Pizza, Cheese, Pepperoni
}
Order matters: Compression and Encryption
Decorators are not commutative. Stacking the same decorators in a different order can produce a different result, a different size, or even a broken behavior. A classic example is a DataSource decorated with compression and encryption.
Assume you are writing a file to disk. Plain text compresses well and encrypted data looks random and therefore compresses poorly. The order of wrapping determines what the disk sees.
Read the nesting carefully. In this codebase each decorator's write() applies its own transform and then forwards to the wrappee (wrappee.write(transform(data))). That means the outermost decorator's transform runs first on write, and the concrete component sees the fully-transformed bytes last. So to compress before encrypting — i.e. store encrypt(compress(x)) — CompressionDecorator must be the outer wrapper and EncryptionDecorator the inner one.
GOOD: Compress, then encrypt
// Compression is the OUTER wrapper, so its transform runs first on write.
DataSource source = new CompressionDecorator(new EncryptionDecorator(new FileDataSource("out.dat")));
source.write("hello hello hello hello"); // disk gets encrypt(compress(x))
The bytes on disk:
- Plain text:
hello hello hello hello(23 bytes). - Compression decorator runs first: repeated pattern collapses to something small, say
[h1e1l2o1 4](about 8 bytes). - Encryption decorator encrypts that small compressed payload (still about 8 bytes, plus IV/tag).
The result is small and secure.
BAD: Encrypt, then compress
// Encryption is the OUTER wrapper, so its transform runs first on write.
DataSource source = new EncryptionDecorator(new CompressionDecorator(new FileDataSource("out.dat")));
source.write("hello hello hello hello"); // disk gets compress(encrypt(x))
The bytes on disk:
- Plain text:
hello hello hello hello(23 bytes). - Encryption decorator runs first: ciphertext looks random, so the repeated pattern is destroyed (about 23 bytes).
- Compression decorator tries to compress random-looking ciphertext: almost no reduction (still about 23 bytes).
Same components, worse result. The lesson: decorator order is part of the design.
A caveat on the demo cipher. The EncryptionDecorator below uses rot13, which is only a placeholder so the example stays runnable. Unlike a real cipher, rot13 is a fixed substitution: it preserves repeated patterns, so it would not actually destroy compressibility. The size claims above ("ciphertext looks random", "almost no reduction") are illustrative — they describe what a real high-entropy cipher such as AES does. AES output is statistically random, which is precisely why compressing after encrypting buys almost nothing, and why the compress-then-encrypt order matters in production.
interface DataSource {
void write(String data);
String read();
}
class FileDataSource implements DataSource {
private String data = "";
private final String path;
public FileDataSource(String path) { this.path = path; }
public void write(String data) { this.data = data; }
public String read() { return data; }
}
abstract class DataSourceDecorator implements DataSource {
protected DataSource wrappee;
public DataSourceDecorator(DataSource source) { this.wrappee = source; }
public void write(String data) { wrappee.write(data); }
public String read() { return wrappee.read(); }
}
class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource source) { super(source); }
public void write(String data) {
// Run-length-like compression for illustration
wrappee.write(compress(data));
}
public String read() {
return decompress(wrappee.read());
}
private String compress(String data) {
if (data.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
int count = 1;
for (int i = 1; i <= data.length(); i++) {
if (i < data.length() && data.charAt(i) == data.charAt(i - 1)) {
count++;
} else {
sb.append(data.charAt(i - 1)).append(count);
count = 1;
}
}
return sb.toString();
}
private String decompress(String data) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < data.length(); i += 2) {
char ch = data.charAt(i);
int count = data.charAt(i + 1) - '0';
for (int j = 0; j < count; j++) sb.append(ch);
}
return sb.toString();
}
}
class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) { super(source); }
public void write(String data) {
wrappee.write(rot13(data));
}
public String read() {
return rot13(wrappee.read());
}
private String rot13(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c >= 'a' && c <= 'z') sb.append((char) ('a' + (c - 'a' + 13) % 26));
else if (c >= 'A' && c <= 'Z') sb.append((char) ('A' + (c - 'A' + 13) % 26));
else sb.append(c);
}
return sb.toString();
}
}
package main
import (
"fmt"
"strconv"
"strings"
)
type DataSource interface {
Write(data string)
Read() string
}
type FileDataSource struct {
path string
data string
}
func (f *FileDataSource) Write(data string) { f.data = data }
func (f *FileDataSource) Read() string { return f.data }
type DataSourceDecorator struct {
source DataSource
}
func (d *DataSourceDecorator) Write(data string) { d.source.Write(data) }
func (d *DataSourceDecorator) Read() string { return d.source.Read() }
type Compression struct { DataSourceDecorator }
func NewCompression(src DataSource) *Compression {
return &Compression{DataSourceDecorator{source: src}}
}
func (c *Compression) Write(data string) { c.source.Write(compress(data)) }
func (c *Compression) Read() string { return decompress(c.source.Read()) }
func compress(s string) string {
if s == "" { return "" }
var sb strings.Builder
count := 1
for i := 1; i <= len(s); i++ {
if i < len(s) && s[i] == s[i-1] {
count++
} else {
sb.WriteByte(s[i-1])
sb.WriteString(strconv.Itoa(count))
count = 1
}
}
return sb.String()
}
func decompress(s string) string {
var sb strings.Builder
for i := 0; i < len(s); i += 2 {
ch := s[i]
count, _ := strconv.Atoi(string(s[i+1]))
for j := 0; j < count; j++ { sb.WriteByte(ch) }
}
return sb.String()
}
type Encryption struct { DataSourceDecorator }
func NewEncryption(src DataSource) *Encryption {
return &Encryption{DataSourceDecorator{source: src}}
}
func (e *Encryption) Write(data string) { e.source.Write(rot13(data)) }
func (e *Encryption) Read() string { return rot13(e.source.Read()) }
func rot13(s string) string {
var sb strings.Builder
for _, r := range s {
switch {
case r >= 'a' && r <= 'z':
sb.WriteRune('a' + (r-'a'+13)%26)
case r >= 'A' && r <= 'Z':
sb.WriteRune('A' + (r-'A'+13)%26)
default:
sb.WriteRune(r)
}
}
return sb.String()
}
func main() {
// Compression is the OUTER wrapper, so it transforms first: disk gets encrypt(compress(x)).
good := NewCompression(NewEncryption(&FileDataSource{path: "out.dat"}))
good.Write("hello hello hello hello")
fmt.Println("compress then encrypt stored:", good.Read())
// Encryption is the OUTER wrapper, so it transforms first: disk gets compress(encrypt(x)).
bad := NewEncryption(NewCompression(&FileDataSource{path: "out.dat"}))
bad.Write("hello hello hello hello")
fmt.Println("encrypt then compress stored:", bad.Read())
}
The canonical anchor: Java I/O streams
The most famous Decorator implementation in the JDK is java.io. InputStream is the Component; concrete streams such as FileInputStream are Concrete Components; FilterInputStream is the abstract Decorator; and BufferedInputStream, GZIPInputStream, DataInputStream, and others are Concrete Decorators.
InputStream in = new GZIPInputStream(
new BufferedInputStream(
new FileInputStream("data.gz")
)
);
Reading from in follows the chain from the outside in:
GZIPInputStreamdecompresses the bytes it receives.BufferedInputStreamreads ahead in chunks to reduce system calls.FileInputStreamreads raw bytes from the file.
Each layer adds one responsibility, and you can reorder or omit layers because they all implement InputStream. The JDK's design is the textbook reference for the pattern.
Decorator vs. Proxy vs. Adapter — three wrappers, three intents
All three patterns are "an object that holds another object and forwards to it," so on a class diagram they look nearly identical. The interviewer's favourite trap is to draw the shared shape and ask which pattern it is — the answer is never in the structure, it is in the intent, and the sharpest single discriminator is what happens to the interface.
| Decorator | Proxy | Adapter | |
|---|---|---|---|
| Interface vs. wrappee | Same — wrapper and wrappee share one type. | Same — the proxy is a stand-in for the real subject. | Different — the whole job is to convert the wrappee's interface into the one the client wants. |
| Purpose | Add responsibilities or transform behavior after forwarding. | Control whether, when, or to whom a call is forwarded. | Make an existing, incompatible class usable through a target interface you cannot change. |
| Forwarding | Almost always forwards; the value is the added behavior. | May skip forwarding entirely (cache hit, permission denied, lazy init). | Always forwards, but translates the call and/or the return shape across the interface gap. |
| Stacking | Designed to nest — same type in, same type out, so you can wrap wrappers arbitrarily deep. | Usually a single layer around the subject. | A single translation layer; you adapt a type once, you do not stack adapters on adapters. |
| Example | BufferedInputStream, the compression/encryption stack above. | Caching query proxy, access-control proxy, virtual proxy. | InputStreamReader (byte-stream → char-stream), Arrays.asList (array → List). |
The one-question test: does the wrapper change what the client receives (Decorator), whether the real object is reached at all (Proxy), or which interface the client talks through (Adapter)? Decorator and Proxy are interface-preserving, which is precisely why they can be stacked and swapped transparently; Adapter is interface-changing by design, which is why it does not nest with itself. Note the two java.io examples sit side by side in the same package: BufferedInputStream is a Decorator (still an InputStream, adds buffering) while InputStreamReader is an Adapter (turns an InputStream into a Reader — a different type entirely). Same library, same wrapping mechanic, opposite answer to "did the type change?"
When to use it — and when not
Use Decorator when you need dynamic, stackable responsibilities behind one stable interface and the features are orthogonal enough to live in separate classes. Good signals: combinations matter, features are added at runtime, or you would otherwise need a combinatorial explosion of subclasses.
Do not use Decorator when:
- A simple subclass or helper method is enough. One topping does not need a decorator hierarchy.
- You only need one feature. A single responsibility can be folded into the class itself or a utility.
- The interface changes. Decorators must implement the full Component interface; if the wrapped type needs a different contract, use Adapter or a different abstraction.
- The behavior is really about access control, lazy initialization, or remote forwarding — that is Proxy territory.
Pitfalls
- Order blindness. Decorators that transform data must be ordered by semantics, not convenience.
- Read must mirror write — in reverse. In a transforming stack the read path unwinds the wrappers in the opposite order, so each decorator's read-transform must be the exact inverse of its write-transform. In the stack above, write applies
compressthenencrypt(outer-to-inner), so read must applydecryptthendecompress(inner-to-outer) — which is what the code does, because each layer'sread()callswrappee.read()first and transforms after. Get one layer's inverse wrong and the data round-trips to garbage; because the stack is opaque to the client, the corruption surfaces far from the layer that caused it, which makes it a nasty on-call debugging session. When you add a transforming decorator, write itsreadandwriteas an inverse pair in the same commit. - Leaky equals/hashCode. A decorator is a different object from the wrapped component; identity checks can surprise you.
- Interface bloat. Every decorator must implement every Component method, even ones it does not care about. Large interfaces make decorators noisy.
- Over-stacking. A tall stack of tiny decorators adds indirection and mental overhead. Sometimes a pipeline or strategy object is clearer.
Takeaways
- Decorator is composition over subclassing. It adds behavior at runtime by wrapping an object in another object that shares the same interface.
- The stack is a linked list of forwards. Each decorator calls the next, and the concrete component sits at the bottom.
- Order is part of the contract. Reordering decorators can change correctness, size, or performance, especially when data is transformed.
- Do not confuse it with Proxy. Decorator enhances the result; Proxy controls access to the real object.
Sources: Gamma, Helm, Johnson & Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Gang of Four), "Decorator" chapter; Java Platform SE API, java.io package. Re-authored and deepened for the Knowledge Guide. The original extracted Pizza example is preserved here as the introductory illustration; the Compression/Encryption ordering example, the Java I/O canonical anchor, the Decorator-vs-Proxy comparison, and the Go implementations are additions.
🤖 Don't fully get this? Learn it with Claude
Stuck on Decorator Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Decorator Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Decorator Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Decorator Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Decorator Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Decorator Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.