June 17, 2026 in patterns5 minutes
A intermediate-level guide to Decorator: before-and-after java code and diagrams for a CS student.
Imagine you are building a coffee shop application. You start with a simple Coffee class that handles the base cost and description. Then, customers want add-ons: milk, sugar, caramel, or whipped cream.
If you use standard inheritance to handle every possible combination, your codebase will quickly spiral out of control. For every new ingredient, you have to create a new subclass. If you have ten ingredients, the number of subclasses required to cover every mathematical combination becomes astronomical. This is known as “class explosion.”
class Coffee {
public String getDescription() { return "Plain Coffee"; }
public double getCost() { return 1.0; }
}
// Class explosion: one subclass for every possible combination of toppings
class CoffeeWithMilk extends Coffee {
public String getDescription() { return getDescription() + ", Milk"; }
public double getCost() { return getCost() + 0.5; }
}
class CoffeeWithSugar extends Coffee {
public String getDescription() { return getDescription() + ", Sugar"; }
public double getCost() { return getCost() + 0.2; }
}
class CoffeeWithMilkAndSugar extends Coffee {
public String getDescription() { return getDescription() + ", Milk and Sugar"; }
public double getCost() { return getCost() + 0.5 + 0.2; }
}
// Imagine needing: WithCaramel, WithWhip, WithSoy, etc. The hierarchy becomes unmanageable.As shown in the diagram below, the hierarchy grows horizontally and vertically for every minor variation, making the system rigid and nearly impossible to maintain:
classDiagram
class Coffee {
+getDescription() String
+getCost() double
}
class CoffeeWithMilk extends Coffee
class CoffeeWithSugar extends Coffee
class CoffeeWithMilkAndSugar extends Coffee
Instead of trying to predict every possible combination using inheritance, we can use Composition. Composition is the practice of holding an object as a field within another class.
Think of it like Russian Nesting Dolls (Matryoshka dolls). You don’t create a “Milk-and-Sugar-Coffee” doll; instead, you take a “Plain Coffee” doll and wrap it in a “Milk” layer, then wrap that entire set in a “Sugar” layer. Each layer adds something new while keeping the inner doll intact.
In software terms, this is the Decorator pattern. To make this work, we need an Interface. An interface is a contract that defines what methods a class must have without specifying how they work. By ensuring both our base object and our “wrappers” implement the same interface, they become interchangeable.
The Decorator pattern uses a clever structural trick: the decorator implements the interface AND contains an instance of that same interface inside itself. This allows you to stack them indefinitely. Because the decorator is-a type of the interface (via implementation) and has-a type of the interface (via composition), it can delegate work to the object it is wrapping.
interface Coffee {
String getDescription();
double getCost();
}
class PlainCoffee implements Coffee {
public String getDescription() { return "Plain Coffee"; }
public double getCost() { return 1.0; }
}
abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
public String getDescription() { return decoratedCoffee.getDescription(); }
public double getCost() { return decoratedCoffee.getCost(); }
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDescription() { return decoratedCoffee.getDescription() + ", Milk"; }
@Override
public double getCost() { return decoratedCoffee.getCost() + 0.5; }
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDescription() { return decoratedCoffee.getDescription() + ", Sugar"; }
@Override
public double getCost() { return decoratedCoffee.getCost() + 0.2; }
}When a method is called on a decorated object, the call travels through the layers like a relay race. The decorator performs its specific logic (like adding $0.5 to the cost) and then calls the same method on the wrapped object to get the rest of the data.
sequenceDiagram
participant C as SugarDecorator
participant M as MilkDecorator
participant P as PlainCoffee
C ->> M: getCost()
M ->> P: getCost()
P -->> M: 1.0
M ->> M: + 0.5
M -->> C: 1.5
C ->> C: + 0.2
C -->> C: 1.7
The class diagram below illustrates how the decorators maintain a relationship with the Coffee interface, allowing them to wrap any object that adheres to that contract:
classDiagram
class Coffee {
<<interface>>
+getDescription() String
+getCost() double
}
class PlainCoffee implements Coffee
class CoffeeDecorator implements Coffee {
#decoratedCoffee : Coffee
}
class MilkDecorator extends CoffeeDecorator
class SugarDecorator extends CoffeeDecorator
CoffeeDecorator --> Coffee : decoratedCoffee
The Decorator pattern is a powerful tool for following the Open/Closed Principle (the idea that classes should be open for extension but closed for modification). You should reach for it when you need to add responsibilities to objects dynamically at runtime.
However, decorators come with costs:
Stacking decorators at runtime to create custom combinations
Coffee myOrder = new SugarDecorator(new MilkDecorator(new PlainCoffee()));
System.out.println(myOrder.getDescription()); // Plain Coffee, Milk, Sugar
System.out.println(myOrder.getCost()); // 1.7