Decorator Pattern: Adding Behavior Without Subclassing
How to attach new behavior to individual objects dynamically, the way java.io streams do, without subclass explosion — and how to chain decorators correctly.
Decorator Pattern
Intent: attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending behavior — behavior is composed at runtime instead of being fixed at compile time by a class hierarchy.
The pattern earns its name from the real-world action: you decorate a cake with frosting, then sprinkles, then a cherry — each layer wraps the previous one, adds its own contribution, and the result is still, fundamentally, a cake you can hand to anyone expecting one. That "still fundamentally the same type, just wrapped" property is exactly what makes Decorator transparent to the client.
If you've used new BufferedReader(new InputStreamReader(new FileInputStream(file))), you've already used Decorator. Each layer wraps the previous Reader/InputStream and is itself a Reader/InputStream — that's the whole pattern, in one line of standard library code you've probably written a hundred times.
1. The problem: behavior combinations cause subclass explosion
Say you're modeling coffee shop orders. A Beverage has a cost() and a description(). Customers can add milk, add a double shot of espresso, add whipped cream, in any combination.
VIOLATION: one subclass per combination
// VIOLATION: every combination of add-ons needs its own subclass
abstract class Beverage {
abstract double cost();
abstract String description();
}
class Espresso extends Beverage {
double cost() { return 2.00; }
String description() { return "Espresso"; }
}
class EspressoWithMilk extends Beverage {
double cost() { return 2.00 + 0.50; }
String description() { return "Espresso, Milk"; }
}
class EspressoWithMilkAndWhip extends Beverage {
double cost() { return 2.00 + 0.50 + 0.70; }
String description() { return "Espresso, Milk, Whip"; }
}
class EspressoWithDoubleShotAndMilkAndWhip extends Beverage {
double cost() { return 2.00 + 2.00 + 0.50 + 0.70; }
String description() { return "Espresso, Double Shot, Milk, Whip"; }
}
// N add-ons → up to 2^N subclasses. Adding a new add-on ("Caramel") means
// writing a new subclass for every EXISTING combination that could include it too.This is a textbook Open/Closed Principle violation with a combinatorial multiplier: a new add-on doesn't just add one class, it potentially doubles the number of classes needed to represent every combination. And every price change to "Milk" means hunting down and editing every subclass whose name contains "Milk."
2. The fix: wrap, don't subclass
Each add-on becomes a decorator: a Beverage that wraps another Beverage, adds its own cost/description contribution, and delegates the rest.
// FIXED: Beverage stays abstract; decorators wrap ANY Beverage, including each other.
abstract class Beverage {
abstract double cost();
abstract String description();
}
class Espresso extends Beverage {
double cost() { return 2.00; }
String description() { return "Espresso"; }
}
// The decorator IS a Beverage (so it's transparent to callers) and HAS a Beverage (so it can wrap one)
abstract class BeverageDecorator extends Beverage {
protected final Beverage wrapped;
BeverageDecorator(Beverage wrapped) { this.wrapped = wrapped; }
}
class MilkDecorator extends BeverageDecorator {
MilkDecorator(Beverage wrapped) { super(wrapped); }
double cost() { return wrapped.cost() + 0.50; }
String description() { return wrapped.description() + ", Milk"; }
}
class WhipDecorator extends BeverageDecorator {
WhipDecorator(Beverage wrapped) { super(wrapped); }
double cost() { return wrapped.cost() + 0.70; }
String description() { return wrapped.description() + ", Whip"; }
}
class ExtraShotDecorator extends BeverageDecorator {
ExtraShotDecorator(Beverage wrapped) { super(wrapped); }
double cost() { return wrapped.cost() + 2.00; }
String description() { return wrapped.description() + ", Extra Shot"; }
}// Any combination, composed at runtime — zero new classes for a new COMBINATION,
// only one new class ever needed for a genuinely new ADD-ON.
Beverage order = new WhipDecorator(
new MilkDecorator(
new ExtraShotDecorator(
new Espresso())));
System.out.println(order.description()); // "Espresso, Extra Shot, Milk, Whip"
System.out.println(order.cost()); // 2.00 + 2.00 + 0.50 + 0.70 = 5.20Adding "Caramel" later is exactly one new class, CaramelDecorator, and it composes with every existing decorator automatically — no combinatorial blow-up.
Decorator chaining works because each decorator is transparently the same type it wraps. order above has static type Beverage at every layer — the caller calling .cost() has no idea, and doesn't need to know, how many decorators are stacked underneath. This transparency is what separates Decorator from a builder that just accumulates a total.
3. Decorator vs. subclassing: runtime vs. compile-time
| Inheritance (subclassing) | Decorator (composition) | |
|---|---|---|
| When behavior is fixed | Compile-time — EspressoWithMilk is permanently that combination | Runtime — wrap and unwrap combinations as needed, even per-request |
| Combining N behaviors | Up to 2^N subclasses for every combination | N decorator classes total, composed as needed |
| Changing behavior on one instance only | Impossible — behavior is a property of the class | Natural — wrap just that one instance |
| New behavior later | New subclass, potentially combined with every existing one | One new decorator class, works with all existing ones automatically |
| Coupling | Tight — subclass depends on superclass internals | Loose — decorator only depends on the abstract component interface |
This table is really the OCP story from the SOLID guide, replayed for the specific "wrap an object at runtime" shape: see SOLID Principles §2 for the general form of "extension via composition, not editing."
4. Java I/O: Decorator in the standard library
java.io is the pattern's most famous real-world instance. InputStream is the abstract component; FileInputStream is a concrete component; FilterInputStream is the abstract decorator; BufferedInputStream, DataInputStream, and GZIPInputStream are concrete decorators.
// Each layer wraps the previous stream, adding one capability:
InputStream stream = new BufferedInputStream( // adds buffering
new GZIPInputStream( // adds decompression
new FileInputStream( // reads raw bytes from disk
"data.csv.gz")));
// Reader side mirrors it:
BufferedReader reader = new BufferedReader( // adds line-based reading + buffering
new InputStreamReader( // adapts bytes -> characters (this layer is actually an Adapter!)
new FileInputStream("data.csv"),
StandardCharsets.UTF_8));Notice InputStreamReader in that stack — it's doing Adapter's job (bytes → characters), not Decorator's job (same interface, added behavior). The two patterns look identical structurally (both wrap an object) but this is a good live example of the difference: InputStreamReader changes the interface from InputStream to Reader; BufferedInputStream keeps the same InputStream interface and adds buffering. See Adapter Pattern for that distinction in depth.
java.io's known wart is instructive too: FilterInputStream's default method implementations delegate naively, which has caused subtle bugs historically (e.g. read(byte[], int, int) not always overridden correctly by custom decorators, mark()/reset() support being decorator-dependent). The lesson for your own decorators: document which methods each decorator overrides, and be explicit about which capabilities pass through unmodified versus which are decorator-specific.
5. When to use vs. when it's overkill
| Use Decorator when | Skip it when |
|---|---|
| You need to add behavior to some instances of a type, not all of them | The behavior applies to every instance of the type uniformly — just put it in the base class |
| The set of possible behavior combinations is large or grows over time | There are only 2-3 fixed, well-known combinations — named subclasses may be clearer |
| You want to add/remove responsibilities at runtime | Behavior is determined entirely at compile/construction time and never changes afterward |
| Behavior needs to compose (order can matter, e.g. compress-then-encrypt vs encrypt-then-compress) | Order never matters and there's exactly one behavior to add — a single wrapper class (no abstract decorator layer) is simpler |
Over-applying Decorator: wrapping a single, fixed piece of cross-cutting behavior (e.g., "always log every repository call") in a decorator hierarchy when a simple AOP aspect, a @Around advice, or even a static utility would do is unnecessary ceremony. Reach for Decorator when combinations genuinely vary per-instance and per-call; reach for a framework's interceptor/aspect mechanism when the behavior is uniform and cross-cutting.
6. Decorator vs. Proxy
Structurally these are nearly identical — both wrap an object behind the same interface. The difference is intent:
| Decorator | Proxy | |
|---|---|---|
| Purpose | Add or enhance behavior | Control access to the underlying object |
| Wrapped object known at construction? | Yes, always passed in and always delegated to | Sometimes lazily created by the proxy itself (virtual proxy) |
| Can be stacked/chained | Yes — that's the point, multiple decorators compose | Rare — usually exactly one proxy in front of the real object |
| Client awareness | Client generally doesn't need to know it's decorated | Client generally doesn't need to know it's a proxy either — but the proxy's job is gatekeeping, not feature-adding |
| Typical example | BufferedInputStream adding buffering | A protection proxy denying delete() calls for non-admin users |
See Proxy Pattern for the full breakdown of proxy variants (virtual, protection, remote) and where the line with Decorator gets blurry in practice (a caching wrapper, for instance, arguably has flavors of both).
7. Production example: request/response middleware
A very common real-world Decorator shape in backend systems — HTTP client wrappers that add retries, metrics, and circuit-breaking around a base HTTP client, each independently toggleable:
interface HttpClient {
HttpResponse send(HttpRequest request);
}
class BasicHttpClient implements HttpClient {
public HttpResponse send(HttpRequest request) { /* actual network call */ return new HttpResponse(200); }
}
abstract class HttpClientDecorator implements HttpClient {
protected final HttpClient wrapped;
HttpClientDecorator(HttpClient wrapped) { this.wrapped = wrapped; }
}
class RetryingHttpClient extends HttpClientDecorator {
private final int maxAttempts;
RetryingHttpClient(HttpClient wrapped, int maxAttempts) { super(wrapped); this.maxAttempts = maxAttempts; }
public HttpResponse send(HttpRequest request) {
RuntimeException lastFailure = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return wrapped.send(request);
} catch (RuntimeException e) {
lastFailure = e;
}
}
throw lastFailure;
}
}
class MetricsHttpClient extends HttpClientDecorator {
MetricsHttpClient(HttpClient wrapped) { super(wrapped); }
public HttpResponse send(HttpRequest request) {
long start = System.nanoTime();
try {
return wrapped.send(request);
} finally {
recordLatency(System.nanoTime() - start);
}
}
private void recordLatency(long nanos) { /* emit to metrics backend */ }
}
// Composed per call site — a background job might skip metrics, a user-facing
// call might want both:
HttpClient resilientClient = new MetricsHttpClient(
new RetryingHttpClient(
new BasicHttpClient(), 3));This is precisely why libraries like OkHttp (Interceptor chains) and Spring's ClientHttpRequestInterceptor are Decorator-shaped under the hood — cross-cutting HTTP concerns compose the same way coffee add-ons do.
Interview Questions
- What problem does Decorator solve that plain subclassing can't, and why does the subclass count grow combinatorially without it?
- Walk through why
new BufferedInputStream(new FileInputStream(...))is a Decorator pattern instance. What's the abstract component, and what's the concrete decorator? - Why is
InputStreamReadertypically called out as an Adapter rather than a Decorator, even though it also "wraps" a stream? - What does it mean for a decorator to be "transparent" to the client? Why does that property matter for chaining?
- Design a decorator-based solution for adding logging, caching, and retry behavior to a service call, and explain why order of wrapping matters.
- Compare Decorator and Proxy. If both wrap an object behind the same interface, what's the deciding question you'd ask to classify a given wrapper as one or the other?
- When would you prefer a framework's AOP/interceptor mechanism over hand-rolling a Decorator hierarchy?
- What's a known pitfall with
java.io'sFilterInputStream-based decorators, and how would you avoid it in your own decorator hierarchy?