Decorator Pattern
Imagine ordering a basic coffee and then adding milk, sugar, whipped cream, caramel, or an extra espresso shot. You do not create an entirely different coffee class for every possible combination. Instead, you start with the basic coffee and wrap it with additional features.
The Decorator Pattern applies the same idea to software. It allows behavior or responsibilities to be added to an individual object dynamically without modifying its original class.
Decorator is a structural design pattern. It uses composition and wrapping to extend an object's behavior while keeping the original interface intact.
Why Does the Decorator Pattern Exist?
A common object-oriented programming problem appears when a class needs many optional features. One tempting solution is to create subclasses for every possible combination.
Suppose a coffee application supports:
- Basic coffee
- Milk
- Sugar
- Whipped cream
- Caramel
Creating separate classes for every combination quickly becomes unpleasant:
CoffeeWithMilk CoffeeWithSugar CoffeeWithMilkAndSugar CoffeeWithMilkSugarAndCream CoffeeWithMilkSugarCreamAndCaramel ...
The number of combinations can grow rapidly as features increase.
Decorator solves this by allowing features to be composed at runtime.
Important: Decorator adds responsibilities to an object without requiring changes to the original class or creating a subclass for every feature combination.
Decorator Pattern in Simple Words
Think of wrapping a gift. You start with the gift itself, then add wrapping paper, a ribbon, and a greeting card. Each layer surrounds the previous one.
The final object still represents the same underlying gift, but it now has additional characteristics.
Software decoration works similarly:
Basic Object
|
v
Decorator A
|
v
Decorator B
|
v
Decorator C
|
v
Final Object
Each decorator can add behavior before or after delegating to the wrapped object.
Core Structure of Decorator
A typical Decorator implementation contains four important participants:
- Component: Defines the common interface.
- Concrete Component: Provides the original behavior.
- Decorator: Implements the same interface and holds a Component.
- Concrete Decorator: Adds a specific responsibility.
A Simple Java Example
Let's begin with a simple coffee interface.
interface Coffee {
String getDescription();
double getCost();
}
The basic coffee is the concrete component.
class BasicCoffee implements Coffee {
@Override
public String getDescription() {
return "Basic Coffee";
}
@Override
public double getCost() {
return 100.0;
}
}
Now create an abstract decorator that also implements Coffee.
abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
A milk decorator can now add its own behavior.
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public double getCost() {
return coffee.getCost() + 20.0;
}
}
A sugar decorator can be added independently.
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return coffee.getCost() + 10.0;
}
}
Now the client can combine decorators dynamically.
Coffee coffee = new BasicCoffee(); coffee = new MilkDecorator(coffee); coffee = new SugarDecorator(coffee); System.out.println(coffee.getDescription()); System.out.println(coffee.getCost());
The resulting description is effectively:
Basic Coffee, Milk, Sugar
And the cost becomes:
130.0
The original BasicCoffee class was never modified.
How the Decorator Works Internally
The most important mechanism is that every decorator holds a reference to the same component abstraction.
SugarDecorator
|
v
MilkDecorator
|
v
BasicCoffee
When the client calls getCost(), the outer decorator can perform its own calculation and delegate to the wrapped object.
For the previous example:
SugarDecorator
+ 10
|
v
MilkDecorator
+ 20
|
v
BasicCoffee
100
The final result is 130.
Remember: A decorator and the object it wraps implement the same component interface. That is what allows decorators to be stacked.
Adding Multiple Decorators
One of the strongest features of the Decorator Pattern is that decorators can be composed in different combinations.
Coffee coffee1 =
new MilkDecorator(
new BasicCoffee()
);
Coffee coffee2 =
new SugarDecorator(
new MilkDecorator(
new BasicCoffee()
)
);
Coffee coffee3 =
new SugarDecorator(
new MilkDecorator(
new BasicCoffee()
)
);
The client can choose the combination at runtime instead of relying on a large inheritance hierarchy.
Order of Decorators Matters
Decorators are not always interchangeable. Their order can affect the final behavior.
Consider decorators that apply discounts:
Price | v Discount A | v Discount B
If Discount A is calculated before Discount B, the result may differ from applying Discount B before Discount A.
Therefore, decorators should not be treated as an unordered collection. Their sequence may be part of the business behavior.
Decorator Can Add Behavior Before Delegation
A decorator does not have to modify the result after delegation. It can execute logic before calling the wrapped component.
class LoggingDecorator implements Coffee {
private final Coffee coffee;
public LoggingDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
System.out.println("Getting description...");
return coffee.getDescription();
}
@Override
public double getCost() {
System.out.println("Calculating cost...");
return coffee.getCost();
}
}
This technique is useful for cross-cutting responsibilities such as logging, metrics, authorization, caching, validation, and tracing.
Decorator Can Add Behavior After Delegation
A decorator can also delegate first and then modify the result.
class TaxDecorator implements Coffee {
private final Coffee coffee;
public TaxDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Tax";
}
@Override
public double getCost() {
double cost = coffee.getCost();
return cost * 1.18;
}
}
Here, the decorator calculates tax using the price produced by the wrapped object.
Decorator for Logging
The pattern becomes especially valuable when a behavior should be attached to selected objects rather than every instance of a class.
interface OrderService {
void placeOrder(String orderId);
}
The real service performs the business operation.
class OrderServiceImpl implements OrderService {
@Override
public void placeOrder(String orderId) {
System.out.println(
"Order placed: " + orderId
);
}
}
A logging decorator can wrap it.
class LoggingOrderService
implements OrderService {
private final OrderService service;
public LoggingOrderService(OrderService service) {
this.service = service;
}
@Override
public void placeOrder(String orderId) {
System.out.println(
"Starting order: " + orderId
);
service.placeOrder(orderId);
System.out.println(
"Finished order: " + orderId
);
}
}
The client can now choose whether an instance should have logging.
OrderService service =
new LoggingOrderService(
new OrderServiceImpl()
);
service.placeOrder("ORD-101");
Decorator for Validation
Another decorator can validate an operation before passing it to the underlying service.
class ValidationOrderService
implements OrderService {
private final OrderService service;
public ValidationOrderService(OrderService service) {
this.service = service;
}
@Override
public void placeOrder(String orderId) {
if (orderId == null || orderId.isBlank()) {
throw new IllegalArgumentException(
"Order ID is required"
);
}
service.placeOrder(orderId);
}
}
Now multiple responsibilities can be composed:
OrderService service =
new LoggingOrderService(
new ValidationOrderService(
new OrderServiceImpl()
)
);
This creates a chain where validation and logging are added without changing the core service.
Decorator Chains
When several decorators are stacked, the result can be thought of as a processing pipeline.
Client | v Logging Decorator | v Validation Decorator | v Caching Decorator | v Core Service
Each layer has one responsibility and can delegate to the next layer.
Architecture Insight: Decorator chains are useful when optional responsibilities need to be composed independently. They can provide a lightweight alternative to creating large inheritance hierarchies.
Decorator vs Inheritance
Both inheritance and decoration can extend behavior, but they do so differently.
| Aspect | Inheritance | Decorator |
|---|---|---|
| Relationship | Class hierarchy | Object composition |
| Behavior selection | Usually fixed by type | Can be selected dynamically |
| Combinations | Can cause subclass explosion | Can be composed through wrapping |
| Runtime flexibility | Lower | Higher |
| Original class modification | Not required | Not required |
Decorator vs Adapter
Adapter and Decorator both use wrapping, but their intent is different.
| Aspect | Adapter | Decorator |
|---|---|---|
| Primary purpose | Make incompatible interfaces compatible | Add responsibilities or behavior |
| Interface | Usually changes the interface presented to the client | Preserves the component interface |
| Main concern | Compatibility | Extension |
| Typical reason | Existing API does not match client expectations | Object needs optional additional behavior |
Decorator vs Proxy
Decorator and Proxy can look almost identical structurally because both can wrap another object and implement the same interface. The difference is mainly their intent.
Decorator focuses on adding responsibilities. Proxy focuses on controlling access to an object.
Decorator:
Client → Decorator → Real Object
|
+ Adds behavior
Proxy:
Client → Proxy → Real Object
|
+ Controls access
Decorator and Open/Closed Principle
Decorator is closely related to the Open/Closed Principle. The principle encourages software entities to be open for extension while being closed for modification.
Instead of repeatedly editing an existing service whenever a new optional behavior is needed, a decorator can introduce that behavior externally.
For example, instead of modifying the original service to add logging, metrics, caching, and validation, separate decorators can provide those responsibilities.
Decorator and Single Responsibility Principle
Decorator can also support the Single Responsibility Principle. Each decorator can focus on one additional responsibility.
Core Service
|
+-- Logging
|
+-- Validation
|
+-- Metrics
|
+-- Caching
Instead of creating one large class responsible for everything, responsibilities can be separated into independently composable decorators.
Real-World Example: Java I/O
Java I/O provides one of the most recognizable examples of decorator-style design.
A basic input stream can be wrapped with another stream that adds buffering or other functionality.
InputStream input =
new BufferedInputStream(
new FileInputStream("data.txt")
);
The underlying file stream provides the basic operation, while the buffered stream adds buffering behavior without changing the fundamental stream abstraction.
This is exactly the kind of compositional thinking that makes the Decorator Pattern powerful.
Dynamic Composition
One of the biggest advantages of Decorator is that the object can be composed differently depending on runtime requirements.
OrderService service =
new OrderServiceImpl();
if (loggingEnabled) {
service = new LoggingOrderService(service);
}
if (validationEnabled) {
service = new ValidationOrderService(service);
}
The same core service can therefore participate in different configurations without creating additional subclasses.
Common Beginner Mistakes
1. Changing the Interface
A classic decorator normally preserves the component interface. If the main purpose is to translate one interface into another, Adapter is usually the more appropriate pattern.
2. Putting Too Much Logic in One Decorator
A decorator should ideally add one coherent responsibility. A single decorator containing logging, caching, validation, authorization, and business rules becomes difficult to understand and maintain.
3. Ignoring Decorator Order
The order of decorators can affect behavior. For example, applying a cache before authorization may have different semantics from applying authorization before the cache.
4. Creating Deeply Nested Decorators Without a Reason
Composition is powerful, but excessive nesting can make debugging and understanding execution flow difficult.
5. Using Decorator When Simple Composition Is Enough
Not every optional operation needs a formal decorator hierarchy. The pattern is most valuable when behaviors share a common interface and need to be independently composable.
Best Practices for Decorator Pattern
- Keep the component interface small and focused.
- Make decorators implement the same interface as the wrapped component.
- Keep each decorator focused on one responsibility.
- Prefer composition instead of creating large inheritance hierarchies.
- Document ordering requirements when decorator order affects behavior.
- Keep the core component independent from optional decorators.
- Test decorators individually and test important decorator combinations.
Testing Decorators
A decorator should be tested for both its own behavior and its interaction with the wrapped component.
For example, a logging decorator should be tested to ensure that it delegates correctly. A validation decorator should verify invalid input without unnecessarily invoking the underlying service.
When multiple decorators are combined, integration tests can verify that the complete chain behaves in the expected order.
When Should You Use Decorator?
- When objects need optional behavior that can be combined dynamically.
- When subclass combinations are becoming difficult to manage.
- When responsibilities should be added without modifying the original class.
- When several independent cross-cutting behaviors can be composed.
- When clients should continue using the same component interface.
When Decorator May Be Unnecessary
- When there is only one fixed implementation and no meaningful variation.
- When a simple method or helper is sufficient.
- When the resulting decorator chain would be harder to understand than the original implementation.
- When the added behavior is inseparable from the core business logic.
Interview Insight: What Problem Does Decorator Solve?
Decorator solves the problem of adding responsibilities to individual objects dynamically without modifying the original class or creating a subclass for every possible combination of behavior.
Interview Insight: Why Does Decorator Use the Same Interface?
The same interface allows a decorator to be treated exactly like the object it wraps. This makes decorators stackable and lets clients remain unaware of how many layers exist underneath.
Interview Insight: Is Decorator Inheritance or Composition?
The core mechanism is composition. A decorator contains another component and delegates operations to it, while adding its own behavior.
Interview Insight: Can Decorators Be Nested?
Yes. Nesting is one of the defining strengths of the pattern. Multiple decorators can wrap one another to form a behavior chain.
Interview Insight: Why Not Create Subclasses?
Subclassing can lead to a large number of classes when several independent optional features can be combined. Decorators allow those features to be assembled dynamically instead.
Learning Checkpoint
Before moving to the next pattern, make sure you can answer these questions:
- What problem does the Decorator Pattern solve?
- Why does a decorator implement the same interface as the component?
- How does composition help avoid subclass explosion?
- Why can decorator order matter?
- How is Decorator different from Adapter?
- How is Decorator different from Proxy?
- How can decorators support the Open/Closed Principle?
Decorator Pattern Quick Revision
| Concept | Key Point |
|---|---|
| Pattern type | Structural |
| Core purpose | Add responsibilities to objects dynamically |
| Component | Common interface for the object and decorators |
| Concrete component | Original object providing core behavior |
| Decorator | Wraps a component and delegates to it |
| Concrete decorator | Adds a specific responsibility |
| Main technique | Composition and delegation |
| Major benefit | Flexible behavior composition without subclass explosion |
| Main caution | Too many layers can make execution flow difficult to understand |
Final Takeaway
The Decorator Pattern lets you extend an object's behavior by wrapping it with additional components that implement the same interface. Each decorator can add one focused responsibility while delegating the rest of the work to the object underneath.
Its real strength appears when behavior is optional, independently composable, and potentially needed in different combinations. Instead of creating a subclass for every combination, decorators let the application build the required behavior at runtime.
The key idea to remember is simple: wrap an object to add behavior while keeping the same interface.
