- Shift Elevate
- Posts
- Decorator Pattern: Add Behaviour Dynamically Without Altering Structure
Decorator Pattern: Add Behaviour Dynamically Without Altering Structure
The Pain of Rigid Class Hierarchies
Picture this: You're building a coffee ordering system where customers can customize their drinks with various add-ons like whipped cream, vanilla, and caramel. Your initial approach seems logical: create separate classes for each combination.
But requirements evolve quickly. As you add more base drinks and add-ons, you'd need to create a separate class for each combination. Your class hierarchy explodes into an unmaintainable mess: code gets duplicated across similar classes, adding a new add-on requires modifying dozens of existing classes, and testing every combination becomes impossible.
Suddenly, you're dealing with a class explosion, violated Open Closed Principle, and a system that's impossible to extend. Sound familiar?
The Decorator pattern solves this challenge by allowing you to attach new behaviour to objects dynamically without altering their structure.
Understanding the Decorator Pattern
The Decorator pattern allows you to attach new behaviour to objects by placing them inside wrapper objects that contain the behaviours. It provides a flexible alternative to subclassing for extending functionality.
Think of it like decorating a Christmas tree: you start with a basic tree, then add lights, ornaments, garlands, and a star. Each decoration wraps around what's already there, adding new features without changing the tree itself. You can mix and match decorations in any combination.
This pattern promotes Flexibility, Open Closed Principle, and Composition over Inheritance while enabling dynamic behaviour addition.

Decorator Pattern Components
Core Components
Component Interface: Defines the interface for objects that can have responsibilities added to them dynamically
Concrete Component: Defines an object to which additional responsibilities can be attached
Base Decorator: Maintains a reference to a Component object and defines an interface that conforms to Component's interface
Concrete Decorators: Add responsibilities to the component
Client Code: Uses the component interface to interact with decorated objects
Complete Java Implementation
Let's build a coffee ordering system that demonstrates the Decorator pattern's power in adding behaviour dynamically without altering structure.
Component Interface
public interface Coffee {
String getDescription();
double getCost();
}Concrete Component
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple coffee";
}
@Override
public double getCost() {
return 2.0;
}
}
public class Espresso implements Coffee {
@Override
public String getDescription() {
return "Espresso";
}
@Override
public double getCost() {
return 2.5;
}
}
public class Latte implements Coffee {
@Override
public String getDescription() {
return "Latte";
}
@Override
public double getCost() {
return 3.0;
}
}Base Decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}Concrete Decorators
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", whipped cream";
}
@Override
public double getCost() {
return coffee.getCost() + 0.8;
}
}
public class VanillaDecorator extends CoffeeDecorator {
public VanillaDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", vanilla";
}
@Override
public double getCost() {
return coffee.getCost() + 0.6;
}
}
public class CaramelDecorator extends CoffeeDecorator {
public CaramelDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", caramel";
}
@Override
public double getCost() {
return coffee.getCost() + 0.7;
}
}Client Code
public class CoffeeShopDemo {
public static void main(String[] args) {
System.out.println("=== Coffee Shop Decorator Pattern Demo ===\n");
System.out.println("=== Simple Coffee Orders ===");
Coffee simpleCoffee = new SimpleCoffee();
System.out.println("Order: " + simpleCoffee.getDescription());
System.out.println("Cost: $" + String.format("%.2f", simpleCoffee.getCost()));
Coffee espresso = new Espresso();
System.out.println("\nOrder: " + espresso.getDescription());
System.out.println("Cost: $" + String.format("%.2f", espresso.getCost()));
System.out.println("\n=== Decorated Coffee Orders ===");
Coffee coffeeWithVanilla = new VanillaDecorator(new SimpleCoffee());
System.out.println("Order: " + coffeeWithVanilla.getDescription());
System.out.println("Cost: $" + String.format("%.2f", coffeeWithVanilla.getCost()));
Coffee fancyLatte = new WhippedCreamDecorator(
new VanillaDecorator(new Latte())
);
System.out.println("\nOrder: " + fancyLatte.getDescription());
System.out.println("Cost: $" + String.format("%.2f", fancyLatte.getCost()));
Coffee ultimateCoffee = new CaramelDecorator(
new WhippedCreamDecorator(
new VanillaDecorator(new Espresso())
)
);
System.out.println("\nOrder: " + ultimateCoffee.getDescription());
System.out.println("Cost: $" + String.format("%.2f", ultimateCoffee.getCost()));
System.out.println("\n=== Dynamic Order Building ===");
Coffee customOrder = buildCustomOrder();
System.out.println("Custom Order: " + customOrder.getDescription());
System.out.println("Cost: $" + String.format("%.2f", customOrder.getCost()));
}
private static Coffee buildCustomOrder() {
Coffee order = new SimpleCoffee();
System.out.println("Building custom order step by step:");
System.out.println("Base: " + order.getDescription() + " - $" + String.format("%.2f", order.getCost()));
order = new VanillaDecorator(order);
System.out.println("Added vanilla: " + order.getDescription() + " - $" + String.format("%.2f", order.getCost()));
order = new CaramelDecorator(order);
System.out.println("Added caramel: " + order.getDescription() + " - $" + String.format("%.2f", order.getCost()));
order = new WhippedCreamDecorator(order);
System.out.println("Added whipped cream: " + order.getDescription() + " - $" + String.format("%.2f", order.getCost()));
return order;
}
}Expected Output
=== Coffee Shop Decorator Pattern Demo ===
=== Simple Coffee Orders ===
Order: Simple coffee
Cost: $2.00
Order: Espresso
Cost: $2.50
=== Decorated Coffee Orders ===
Order: Simple coffee, vanilla
Cost: $2.60
Order: Latte, vanilla, whipped cream
Cost: $4.40
Order: Espresso, vanilla, whipped cream, caramel
Cost: $4.60
=== Dynamic Order Building ===
Building custom order step by step:
Base: Simple coffee - $2.00
Added vanilla: Simple coffee, vanilla - $2.60
Added caramel: Simple coffee, vanilla, caramel - $3.30
Added whipped cream: Simple coffee, vanilla, caramel, whipped cream - $4.10
Custom Order: Simple coffee, vanilla, caramel, whipped cream
Cost: $4.10🚀 Get the Complete Implementation
The full code with advanced decorator implementations and dynamic behaviour addition is available in our Design Patterns Repository.
# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=DecoratorPatternTestReal World Examples
The Decorator pattern can be an excellent fit for various real world scenarios:
1. GUI Component Enhancement
The Decorator pattern works particularly well for adding visual features to UI components. A basic window component could be wrapped with a BorderDecorator for borders, then a ScrollDecorator for scrollbars, then a ShadowDecorator for drop shadows—each decorator adding specific visual functionality. Modern UI frameworks like Java Swing and Android's View system use this approach extensively, allowing developers to combine features like scrolling, borders, and styling without creating separate classes for every combination.
2. HTTP Request/Response Pipeline
The Decorator pattern can be applied to build flexible HTTP middleware. A basic HTTP request handler could be wrapped with a CompressionDecorator for GZIP compression, then an AuthenticationDecorator for adding auth tokens, then a CachingDecorator for response caching, then a LoggingDecorator for request tracking—each adding specific functionality without modifying the core handler. This approach powers frameworks like Express.js middleware and Spring's HandlerInterceptor chain.
3. Notification Systems
The Decorator pattern is well-suited for building robust notification delivery systems. A basic notification sender could be wrapped with a RetryDecorator for automatic retries on failure, then a RateLimitingDecorator to prevent API throttling, then a MetricsDecorator for tracking delivery rates—each adding reliability features. This enables composing complex notification behaviors (email with retry logic, SMS with rate limiting, push notifications with fallback) without creating separate classes for each combination.
When to Use Decorator Pattern
Understanding when to apply the Decorator pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:
✅ Ideal Scenarios:
You want to add features that can be combined in various ways.
You need to extend functionality without subclassing.
You want to add responsibilities to individual objects without affecting others.
❌ Skip It When:
You need to add behaviour to an entire class hierarchy.
The decorators need to know about each other.
You have a small number of simple combinations.
Performance is critical and the overhead of multiple decorators is too high.
Next Steps: Apply Decorator Pattern in Your Project
Ready to implement the Decorator pattern in your own projects? Here's a structured approach to get you started:
Identify Flexible Behaviour: Look for areas where you need to add features dynamically.
Define Component Interface: Create a common interface for the base object and decorators.
Implement Base Component: Build the core functionality that can be decorated.
Create Abstract Decorator: Build the base decorator class that wraps components.
Implement Concrete Decorators: Build specific decorators for each behaviour you want to add.
The Decorator pattern transforms rigid class hierarchies into flexible, composable systems. By wrapping objects with decorators, you build applications that can add features dynamically without modifying existing code.
Found this helpful? Share it with a colleague who's struggling with rigid class hierarchies. Got questions? We'd love to hear from you at [email protected].