Overview
The Decorator pattern adds responsibilities to an object dynamically by wrapping it in another object with the same interface. Each decorator does its extra work and delegates to the wrapped object. Decorators stack: new Logging(new Retry(new Caching(httpClient))).
Decorators provide a flexible alternative to subclassing for extending behavior, avoiding class explosion for combinations of features. They are the object-oriented form of middleware and are perfect for cross-cutting concerns such as logging, caching, metrics, retries, authorization, and compression.
You put on a shirt, then a sweater, then a raincoat. Each layer adds something (warmth, waterproofing) while you are still the same person, and layers can be combined in any order.
When to use it
- Add features in combinations (milk + sugar + caramel).
- Cross-cutting concerns around an existing interface.
- You cannot or should not modify the original class.
Where it shows up in interviews
Recognize it when: base item plus optional extras.
- Design a coffee machine
- Design a pizza ordering system
Recognize it when: logging, caching, retry around a service.
- Design an HTTP client library
- Design a caching layer for a repository
Where it is used in real software
new BufferedInputStream(new GZIPInputStream(new FileInputStream(f))) stacks decorators.
Each middleware wraps the next handler with extra behavior.
@lru_cache and @retry wrap functions with caching and retry behavior.
Key terms
- Component
- Common interface (Beverage, HttpClient).
- Concrete component
- The base object being decorated.
- Decorator
- Implements the interface and wraps a component.
- Stacking
- Multiple decorators wrapping each other.
How it works, step by step
- 1Identify the component interface
cost(), description().
- 2Implement the base component
Espresso.
- 3Create a base decorator
Holds a component and delegates.
- 4Add concrete decorators
Milk, Caramel add behavior around delegation.
- 5Compose at runtime
new Caramel(new Milk(new Espresso())).
Coffee with add-ons
new Caramel(new Milk(new Espresso()))
| Layer | cost() | description() |
|---|---|---|
| Espresso | 2.00 | Espresso |
| Milk(Espresso) | 2.00 + 0.50 = 2.50 | Espresso, milk |
| Caramel(Milk(...)) | 2.50 + 0.70 = 3.20 | Espresso, milk, caramel |
NOWLayer: Espresso | cost(): 2.00 | description(): Espresso
Each decorator adds its part and delegates the rest; any combination works without new subclasses.
Implementation
public interface Beverage { long costCents(); String description();} public final class Espresso implements Beverage { public long costCents() { return 200; } public String description() { return "Espresso"; }} public abstract class AddOn implements Beverage { protected final Beverage inner; protected AddOn(Beverage inner) { this.inner = inner; }} public final class Milk extends AddOn { public Milk(Beverage inner) { super(inner); } public long costCents() { return inner.costCents() + 50; } public String description() { return inner.description() + ", milk"; }} public final class Caramel extends AddOn { public Caramel(Beverage inner) { super(inner); } public long costCents() { return inner.costCents() + 70; } public String description() { return inner.description() + ", caramel"; }} Beverage order = new Caramel(new Milk(new Espresso())); // 320 centsComplexity and performance
One delegation each.
vs 2^n subclasses.
Trade-offs
Deep stacks of wrappers are harder to debug and stack traces get longer.
Logging outside caching logs every call; inside it logs only misses.
Variants and related techniques
Higher-order functions that wrap functions.
Same structure; Proxy controls access, Decorator adds behavior.
Common mistakes
- Decorators that change the interface.
Fix: That is an Adapter; decorators keep the same interface.
- Relying on the concrete type after decorating.
Fix: instanceof Espresso fails on a decorated object; use the interface.
Interview questions
Decorator vs inheritance for adding features?
Inheritance fixes features at compile time and needs a subclass per combination. Decorators add features at runtime by wrapping, and any combination is possible with one class per feature.
How would you add caching and retries to a repository without changing it?
Wrap it in decorators implementing the same interface: a CachingRepository that checks the cache before delegating and a RetryingRepository that retries transient failures.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Coffee machine with add-ons | Easy | Stacking. |
| Decorators for logging, retry, and metrics on a client | Medium | Order of wrapping. |