Decorator Pattern: Adding Behaviour by Wrapping
How decorators add features to an object by wrapping it with the same interface. A coffee example stacks milk and sugar to 2.70, and a Java GZIP wrapper shrank 9,000 bytes of text to 108.
The decorator pattern wraps an object with another object that implements the same interface and adds behaviour before or after delegating to it. Interviewers expect the contrast with inheritance: decorators are combined at run time and can be stacked in any order, so features combine without a subclass for every combination, as in Java's BufferedOutputStream and GZIPOutputStream.
A decorator implements the same interface as the object it wraps and adds behaviour before or after delegating to it. Wrappers can be stacked in any order without subclass explosion. A coffee example composed to 2.70, and wrapping a Java output stream in GZIPOutputStream reduced 9,000 bytes of repeated text to 108.
- A decorator implements the same interface as the object it wraps and adds behaviour before or after delegating, so wrappers can be stacked in any number and order.
- In the Java coffee example, new Sugar(new Milk(new Basic())) prints coffee, milk, sugar = 2.7, with each wrapper adding its cost on the way back.
- Writing the same 9,000-byte repeated text through a BufferedOutputStream produced 9,000 bytes, while adding a GZIPOutputStream decorator produced 108 bytes.
- A decorator keeps the interface and adds responsibilities, a proxy controls access to its target, and an adapter changes the interface to make a class fit.
- Decorator order can change the result: compress-then-encrypt differs from encrypt-then-compress, because encrypted data does not compress.
01The problem
Imagine coffee with optional extras: milk, sugar, whipped cream. If each combination is a subclass (MilkCoffee, MilkSugarCoffee, SugarCreamCoffee...), the number of classes doubles with every new extra. Inheritance fixes the combination when you write the code, not when the program runs.
The decorator pattern instead lets you combine features at run time by wrapping. Each extra is a small class that holds another coffee and adds its own contribution.
02The structure
interface Coffee { double cost(); String desc(); }
record Basic() implements Coffee {
public double cost() { return 2.0; } public String desc() { return "coffee"; }
}
record Milk(Coffee c) implements Coffee {
public double cost() { return c.cost() + 0.5; }
public String desc() { return c.desc() + ", milk"; }
}
record Sugar(Coffee c) implements Coffee {
public double cost() { return c.cost() + 0.2; }
public String desc() { return c.desc() + ", sugar"; }
}
Coffee c = new Sugar(new Milk(new Basic()));Printing c.desc() + " = " + c.cost() gave coffee, milk, sugar = 2.7 (2.70 when formatted with %.2f). Sugar calls into Milk, which calls into Basic, and each adds its share on the way back. The caller only sees a Coffee. Because every decorator is also a Coffee, wrappers can be stacked in any number and order, and a new extra is one new class that touches nothing else.
03The real-world example: Java I/O
ByteArrayOutputStream raw = new ByteArrayOutputStream(), gz = new ByteArrayOutputStream();
OutputStream plain = new BufferedOutputStream(raw);
OutputStream zipped = new BufferedOutputStream(new GZIPOutputStream(gz));
// write the same bytes to each, then plain.close(); zipped.close();
// close() is required: gzip only finishes its output (trailer) on closeA FileOutputStream writes bytes. BufferedOutputStream wraps it to add buffering. GZIPOutputStream wraps it to compress. DataOutputStream adds typed writes. They all extend OutputStream, so you stack what you need. In a test I wrote the same 9,000-byte text (a sentence repeated 200 times) through each setup. The plain buffered stream produced 9,000 bytes. With the gzip decorator added the output was 108 bytes. Repeated text compresses extremely well, so real data will give smaller savings. The calling code was identical in both cases; only the wrapping differed.
04Decorators in web code
Web frameworks use the same idea under other names. Middleware in Express, Django or ASP.NET wraps a request handler with logging, authentication or compression, each layer calling the next. Python's @decorator syntax wraps a function with extra behaviour (caching, timing, access checks). Java annotations processed by frameworks, and proxies that add transaction handling, apply the pattern at a larger scale.
In each case the wrapped thing and the wrapper look the same to the caller, which is why the pattern composes so well.
05Pitfalls
Order can change the result. Compress-then-encrypt is very different from encrypt-then-compress (encrypted data does not compress). A cache layer around an authentication layer behaves differently from authentication around a cache. Document the intended stacking.
Identity: the wrapper is a different object from the thing wrapped, so instanceof and equality checks can surprise you, and the wrapper only exposes methods that are in the shared interface. If the interface is large, writing each decorator means forwarding many methods.
Debugging a deep stack of wrappers is harder, since a stack trace passes through every layer. Keep decorators small and focused.
06Decorator versus similar patterns
A proxy has the same interface too, but controls access (lazy loading, remote calls, permissions) rather than adding features, and usually creates or hides its target. An adapter changes the interface to make an incompatible class fit. A decorator keeps the interface and adds responsibilities. Compared with subclassing, decoration is chosen at run time, allows combinations without a class for each, and can be applied to any object implementing the interface, at the cost of more small objects.
07Common interview questions
Q: What is the decorator pattern?
A: Wrapping an object with another that has the same interface to add behaviour dynamically, as an alternative to subclassing.
Q: How is it different from inheritance?
A: Decoration happens at run time and can combine features freely. Inheritance fixes behaviour at compile time and needs a subclass per combination.
Q: Give a Java example.
A: BufferedInputStream or GZIPOutputStream wrapping another stream.
Q: Decorator versus proxy?
A: A decorator adds responsibilities. A proxy controls access to the object.
Q: Decorator versus adapter?
A: An adapter changes the interface. A decorator keeps it.
Q: A downside?
A: Many small objects, order sensitivity, and harder debugging through layers.