Decorator Design Pattern

Software systems rarely stay the same for long. Features evolve, requirements change, and behavior that was once sufficient often needs to be extended.
One common challenge is adding new functionality to an existing object without modifying its code. At first glance, this may seem simple, but it can quickly lead to rigid designs and class explosion if not handled carefully.
The Decorator Design Pattern addresses this problem by allowing behavior to be added dynamically to individual objects while keeping the original class unchanged.
The key idea is composition instead of inheritance.
Rather than creating multiple subclasses for every possible combination of features, we wrap an object inside another object that implements the same interface. Each wrapper (decorator) adds its own behavior and delegates the remaining work to the wrapped object.
The Problem
Imagine a coffee shop application.
Initially, we only have a simple coffee.
Coffee coffee = new SimpleCoffee();
Later, customers want additional options:
Milk
Sugar
Whipped Cream
A common approach is inheritance:
SimpleCoffee
├─ CoffeeWithMilk
├─ CoffeeWithSugar
├─ CoffeeWithWhip
├─ CoffeeWithMilkAndSugar
├─ CoffeeWithMilkAndWhip
└─ CoffeeWithMilkSugarAndWhip
As more toppings are added, the number of classes grows quickly. Maintaining these combinations becomes difficult and inflexible.
The real issue is that toppings are optional behaviors that should be added dynamically, not fixed through inheritance.
The Solution
Decorator solves this by wrapping an object inside another object.
The structure looks like this:
Component (Coffee)
↑
SimpleCoffee
Decorator
↑
MilkDecorator
SugarDecorator
WhipDecorator
Every decorator implements the same interface as the original object and delegates work to the wrapped object while adding its own behavior.
Coffee Example
First, define the common interface.
public interface Coffee {
double getCost();
String getDescription();
}
Base coffee implementation:
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 100;
}
@Override
public String getDescription() {
return "Simple Coffee";
}
}
Abstract decorator:
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost();
}
@Override
public String getDescription() {
return coffee.getDescription();
}
}
Milk decorator:
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 50;
}
@Override
public String getDescription() {
return super.getDescription() + ", Milk";
}
}
Sugar decorator:
public class SugarDecorator extends CoffeeDecorator {
@override
double getCost() {
return coffee.getCost() + 20.00;
}
@override
String getDescription() {
return coffee.getDescription + ", Sugar";
}
}
Whip decorator:
public class WhipDecorator extends CoffeeDecorator {
@override
double getCost() {
return coffee.getCost() + 70.00;
}
@override
String getDescription() {
return coffee.getDescription + ", Whip";
}
}
Building a Coffee
public class Main {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
print(coffee);
coffee = new MilkDecorator(coffee);
print(coffee);
coffee = new SugarDecorator(coffee);
print(coffee);
coffee = new WhipDecorator(coffee);
print(coffee);
Coffee special = new WhipDecorator(
new SugarDecorator(
new MilkDecorator{
new SimpleCoffee())));
print(special);
}
}
Coffee coffee =
new WhipDecorator(
new SugarDecorator(
new MilkDecorator(
new SimpleCoffee()
)
)
);
Visually:
WhipDecorator
↓
SugarDecorator
↓
MilkDecorator
↓
SimpleCoffee
When getCost() is called, each layer contributes its own cost.
Simple Coffee = 100
Milk = +50
Sugar = +20
Whip = +70
-------------------
Total = 240
The client only interacts with the outermost object and remains unaware of the internal chain.
Why Decorator Works Well
Runtime Flexibility
Features can be added dynamically.
coffee = new MilkDecorator(coffee);
Avoids Class Explosion
We don't need separate classes for every topping combination.
Follows Open-Closed Principle
Existing classes remain unchanged while new functionality can be added through new decorators.
Encourages Composition
Behavior is assembled from small reusable components rather than inherited from large hierarchies.
Tradeoffs
Decorator is not free.
A heavily decorated object can become deeply nested:
new WhipDecorator(
new SugarDecorator(
new MilkDecorator(
new SimpleCoffee()
)
)
);
This can make debugging slightly harder because method calls pass through multiple layers.
Also, the order of decorators can matter in some scenarios, especially when decorators modify behavior rather than simply add values.
Summary
The Decorator Pattern is a clean way to extend functionality without modifying existing classes. It replaces rigid inheritance hierarchies with flexible composition, making systems easier to extend as requirements evolve.
Whenever you see many subclasses being created just to support different combinations of features, it is often a sign that a decorator-based design may be a better solution.



