Decorator Design Pattern in Java: Explained with a Simple Coffee Example
The Decorator Design Pattern is one of the most useful structural design patterns in software development.
It allows us to add new features or responsibilities to an existing object without modifying its original class.
In simple words:
The Decorator Pattern adds extra functionality to an object by wrapping it inside another object.
Let us understand this pattern using a simple coffee shop example.
1. Real-Life Example
Imagine that you visit a coffee shop and order a plain coffee.
The plain coffee costs ₹50.
You may also add optional items such as:
Milk
Sugar
Cream
Chocolate
Caramel
Your final order may look like this:
Plain Coffee
Plain Coffee + Milk
Plain Coffee + Milk + Sugar
Plain Coffee + Milk + Sugar + Cream
The basic coffee remains the same. We are only adding extra features to it.
Each extra item decorates the original coffee.
This is the main idea behind the Decorator Design Pattern.
2. The Problem Without the Decorator Pattern
Suppose we create a separate class for every possible coffee combination.
class PlainCoffee {
}
class MilkCoffee {
}
class SugarCoffee {
}
class MilkSugarCoffee {
}
class MilkSugarCreamCoffee {
}
This approach may work when there are only two or three combinations.
However, imagine that the coffee shop supports the following toppings:
Milk
Sugar
Cream
Chocolate
Caramel
Honey
Vanilla
The number of possible combinations will increase rapidly.
We may need classes such as:
MilkCoffee
SugarCoffee
CreamCoffee
MilkSugarCoffee
MilkCreamCoffee
SugarCreamCoffee
MilkSugarCreamCoffee
ChocolateMilkCoffee
CaramelSugarCoffee
This creates too many classes.
This problem is known as class explosion.
The Decorator Pattern solves this problem by allowing us to combine small reusable decorators at runtime.
3. Definition of the Decorator Pattern
The Decorator Pattern is a structural design pattern that dynamically adds new responsibilities to an object by wrapping it inside another object that follows the same interface.
A simpler definition is:
The Decorator Pattern adds new behaviour to an object without changing the original class.
Instead of creating multiple subclasses, we create small wrapper classes that add one responsibility at a time.
4. Main Components of the Decorator Pattern
The Decorator Pattern generally contains four main components:
Component interface
Concrete component
Base decorator
Concrete decorators
In our coffee example:
| Pattern Component | Coffee Example |
|---|---|
| Component | Coffee |
| Concrete Component | PlainCoffee |
| Base Decorator | CoffeeDecorator |
| Concrete Decorators | MilkDecorator, SugarDecorator, CreamDecorator |
Let us implement each component step by step.
5. Step 1: Create the Component Interface
The component interface defines the common operations that will be supported by both the original object and its decorators.
interface Coffee {
String getDescription();
int getCost();
}
Every type of coffee must provide:
getDescription();
getCost();
The important point is that both the original coffee and all decorators will implement the same Coffee interface.
6. Step 2: Create the Concrete Component
The concrete component is the original object that we want to decorate.
class PlainCoffee implements Coffee {
@Override
public String getDescription() {
return "Plain Coffee";
}
@Override
public int getCost() {
return 50;
}
}
Here:
Plain Coffee cost = ₹50
PlainCoffee is the basic object.
Milk, sugar, cream, and other features will be added around this object.
7. Step 3: Create the Base Decorator
The base decorator implements the same Coffee interface and stores a reference to another Coffee object.
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
The most important line is:
protected Coffee coffee;
This means that a decorator contains another Coffee object.
For example:
new MilkDecorator(new PlainCoffee());
Here, MilkDecorator wraps the PlainCoffee object.
This wrapping mechanism is the heart of the Decorator Pattern.
8. Step 4: Create the Milk Decorator
The milk decorator adds milk to an existing coffee.
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Milk";
}
@Override
public int getCost() {
return coffee.getCost() + 20;
}
}
Milk costs ₹20.
The decorator first asks the wrapped coffee for its current cost:
coffee.getCost();
It then adds the milk cost:
coffee.getCost() + 20;
Similarly, it adds " + Milk" to the description.
9. Create the Sugar Decorator
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Sugar";
}
@Override
public int getCost() {
return coffee.getCost() + 10;
}
}
Sugar costs ₹10.
The decorator adds ₹10 to the cost of the wrapped coffee.
10. Create the Cream Decorator
class CreamDecorator extends CoffeeDecorator {
public CreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Cream";
}
@Override
public int getCost() {
return coffee.getCost() + 30;
}
}
Cream costs ₹30.
It adds cream to the description and ₹30 to the existing cost.
11. Complete Java Program
interface Coffee {
String getDescription();
int getCost();
}
class PlainCoffee implements Coffee {
@Override
public String getDescription() {
return "Plain Coffee";
}
@Override
public int getCost() {
return 50;
}
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Milk";
}
@Override
public int getCost() {
return coffee.getCost() + 20;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Sugar";
}
@Override
public int getCost() {
return coffee.getCost() + 10;
}
}
class CreamDecorator extends CoffeeDecorator {
public CreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Cream";
}
@Override
public int getCost() {
return coffee.getCost() + 30;
}
}
public class Main {
public static void main(String[] args) {
Coffee coffee = new PlainCoffee();
System.out.println(coffee.getDescription());
System.out.println("Cost: Rs. " + coffee.getCost());
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new CreamDecorator(coffee);
System.out.println();
System.out.println(coffee.getDescription());
System.out.println("Cost: Rs. " + coffee.getCost());
}
}
Output:
Plain Coffee
Cost: Rs. 50
Plain Coffee + Milk + Sugar + Cream
Cost: Rs. 110
The final cost is calculated as:
Plain Coffee = ₹50
Milk = ₹20
Sugar = ₹10
Cream = ₹30
-------------------
Total = ₹110
12. How Does Object Wrapping Work?
Consider the following code:
Coffee coffee = new PlainCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new CreamDecorator(coffee);
After the first line:
coffee → PlainCoffee
After adding milk:
coffee → MilkDecorator → PlainCoffee
After adding sugar:
coffee → SugarDecorator → MilkDecorator → PlainCoffee
After adding cream:
coffee → CreamDecorator → SugarDecorator → MilkDecorator → PlainCoffee
The same object structure can also be created in one statement:
Coffee coffee =
new CreamDecorator(
new SugarDecorator(
new MilkDecorator(
new PlainCoffee()
)
)
);
The outermost object is CreamDecorator.
Inside it, there is a SugarDecorator.
Inside the sugar decorator, there is a MilkDecorator.
Inside the milk decorator, there is a PlainCoffee.
13. Understanding the getCost() Call Flow
When we execute:
coffee.getCost();
The call starts from the outermost decorator.
CreamDecorator.getCost()
↓
SugarDecorator.getCost()
↓
MilkDecorator.getCost()
↓
PlainCoffee.getCost()
Now the values return in the opposite direction.
Plain coffee
PlainCoffee returns 50
Milk decorator
50 + 20 = 70
Sugar decorator
70 + 10 = 80
Cream decorator
80 + 30 = 110
Therefore, the final result is:
₹110
We can represent the calculation as:
CreamDecorator.getCost()
= SugarDecorator.getCost() + 30
= MilkDecorator.getCost() + 10 + 30
= PlainCoffee.getCost() + 20 + 10 + 30
= 50 + 20 + 10 + 30
= 110
This recursive delegation is the central working mechanism of the Decorator Pattern.
15. Important Relationships in the Decorator Pattern
Consider the following class:
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
}
This class has two important relationships with Coffee.
IS-A Relationship
CoffeeDecorator implements Coffee
Therefore:
CoffeeDecorator IS-A Coffee
A decorator can be used wherever a Coffee object is expected.
For example:
Coffee coffee = new MilkDecorator(new PlainCoffee());
HAS-A Relationship
protected Coffee coffee;
Therefore:
CoffeeDecorator HAS-A Coffee
The decorator stores and wraps another coffee object.
This is why we often describe the Decorator Pattern using the following statement:
A decorator is a component and also contains a component.
In this example:
CoffeeDecorator IS-A Coffee
CoffeeDecorator HAS-A Coffee
16. Why Does the Decorator Implement the Same Interface?
The decorator implements the Coffee interface so that it can replace the original object.
Both of the following objects are treated as Coffee:
Coffee coffee1 = new PlainCoffee();
Coffee coffee2 =
new MilkDecorator(
new PlainCoffee()
);
Because every decorator is also a Coffee, decorators can be nested inside one another.
Coffee coffee =
new SugarDecorator(
new MilkDecorator(
new PlainCoffee()
)
);
Without the common interface, this chaining would not be possible.
17. Why Not Use Inheritance?
We could try to solve the problem using inheritance.
class Coffee {
}
class MilkCoffee extends Coffee {
}
class SugarCoffee extends Coffee {
}
class MilkSugarCoffee extends Coffee {
}
class MilkSugarCreamCoffee extends Coffee {
}
However, this creates a separate class for every combination.
As the number of optional features grows, the number of subclasses also grows.
The Decorator Pattern uses composition instead.
new MilkDecorator(
new PlainCoffee()
);
Or:
new SugarDecorator(
new MilkDecorator(
new PlainCoffee()
)
);
With decorators, existing features can be combined in different ways without creating new classes for every combination.
The main difference is:
Inheritance adds behaviour through subclasses.
Decorator adds behaviour through object composition.
Inheritance usually defines behaviour at compile time.
Decorator allows behaviour to be added dynamically at runtime.
18. Benefits of the Decorator Pattern
Dynamic Behaviour
Features can be added while the program is running.
Coffee coffee = new PlainCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
The final combination can depend on the user's choices.
No Modification of the Original Class
PlainCoffee does not need to know anything about milk, sugar, or cream.
New features are added through separate decorators.
Avoids Class Explosion
We do not need a separate class for every possible combination.
The same decorators can be reused and combined.
Supports the Open/Closed Principle
The Open/Closed Principle states:
Software entities should be open for extension but closed for modification.
We can add a new decorator without changing the existing classes.
class ChocolateDecorator extends CoffeeDecorator {
public ChocolateDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + " + Chocolate";
}
@Override
public int getCost() {
return coffee.getCost() + 25;
}
}
No changes are required in:
PlainCoffee
MilkDecorator
SugarDecorator
CreamDecorator
Supports the Single Responsibility Principle
Each decorator has one responsibility.
MilkDecorator → Adds milk
SugarDecorator → Adds sugar
CreamDecorator → Adds cream
This makes the code easier to understand and maintain.
Follows Composition Over Inheritance
Instead of creating complex inheritance hierarchies, the Decorator Pattern combines objects.
19. Disadvantages of the Decorator Pattern
The Decorator Pattern is useful, but it also has some disadvantages.
Many Small Objects
A decorated object may contain several wrapper objects.
CreamDecorator
SugarDecorator
MilkDecorator
PlainCoffee
This can create many small objects.
Debugging Can Become Difficult
Deeply nested decorators can make the call flow harder to understand.
new A(
new B(
new C(
new D(object)
)
)
);
Decorator Order May Matter
In some systems, changing the order of decorators may change the result.
For example:
new CompressionDecorator(
new EncryptionDecorator(data)
);
This means:
Encrypt first, then compress.
But:
new EncryptionDecorator(
new CompressionDecorator(data)
);
This means:
Compress first, then encrypt.
These two operations may produce different results.
Object Creation Can Look Complex
A deeply decorated object may be difficult to read when created in one statement.
Using step-by-step wrapping can improve readability.
Coffee coffee = new PlainCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new CreamDecorator(coffee);
20. When Should We Use the Decorator Pattern?
Use the Decorator Pattern when:
Features need to be added dynamically.
Many optional combinations are possible.
The original class should not be modified.
Inheritance is creating too many subclasses.
Features should be reusable and independent.
Responsibilities need to be added one at a time.
Different users may select different combinations of features.
Common examples include:
Coffee and pizza toppings
Notification channels
File compression
File encryption
Logging
Authentication
Authorization
Web middleware
Java InputStream classes
Graphical user-interface components
21. Real Java Example: InputStream
Java's I/O library is one of the most popular real-world examples of the Decorator Pattern.
InputStream inputStream =
new BufferedInputStream(
new FileInputStream("data.txt")
);
Here:
FileInputStream reads data from the file.
BufferedInputStream adds buffering behaviour.
More decorators can be added:
DataInputStream inputStream =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.txt")
)
);
The wrapping structure is:
DataInputStream
└── BufferedInputStream
└── FileInputStream
Each wrapper adds new functionality while still behaving like an input stream.
22. Another Example: Notification System
Suppose a basic notification system sends an email.
We may want to add:
SMS notification
WhatsApp notification
Slack notification
Push notification
The wrapping structure may look like this:
SlackNotificationDecorator
└── WhatsAppNotificationDecorator
└── SMSNotificationDecorator
└── EmailNotification
When the notification method is called, every decorator performs its responsibility.
This allows one notification to be sent through multiple channels without creating classes such as:
EmailAndSMSNotification
EmailAndWhatsAppNotification
EmailSMSAndSlackNotification
23. Decorator Pattern vs Strategy Pattern
Both patterns use composition, but their purposes are different.
Strategy Pattern
The Strategy Pattern selects one behaviour from multiple available behaviours.
For example:
UPI Payment
Credit Card Payment
Cash on Delivery
Usually, one strategy is selected at a time.
paymentService.setPaymentStrategy(
new UpiPayment()
);
Decorator Pattern
The Decorator Pattern adds multiple responsibilities around an existing object.
Coffee + Milk + Sugar + Cream
Multiple decorators can be chained together.
The simplest difference is:
Strategy selects or replaces behaviour.
Decorator adds behaviour.
24. Decorator Pattern vs Proxy Pattern
Decorator and Proxy may appear similar because both wrap another object.
However, their intentions are different.
Decorator
A decorator adds new functionality.
Example:
Plain Coffee + Milk
Proxy
A proxy controls access to an object.
It may provide:
Permission checking
Lazy loading
Caching
Remote communication
Logging
Security
The simplest difference is:
Decorator enhances behaviour.
Proxy controls access.
25. Decorator Pattern vs Adapter Pattern
The Adapter Pattern changes an object's interface so that it becomes compatible with another system.
The Decorator Pattern keeps the same interface and adds new behaviour.
Adapter changes the interface.
Decorator keeps the interface but adds behaviour.
26. Interview Definition
A good interview definition is:
The Decorator Pattern is a structural design pattern that dynamically adds new responsibilities to an object by wrapping it inside another object that implements the same interface. It provides a flexible alternative to subclassing.
A simpler interview answer is:
The Decorator Pattern adds extra behaviour to an object at runtime without modifying its original class.
27. How to Explain the Code in an Interview
You can explain the coffee example like this:
Coffeeis the common component interface.PlainCoffeeis the concrete component that provides the basic behaviour.CoffeeDecoratoris an abstract decorator that implements the same interface and stores a reference to anotherCoffeeobject.MilkDecorator,SugarDecorator, andCreamDecoratorare concrete decorators. Each decorator delegates the operation to the wrapped object and adds its own additional behaviour. This allows us to create different coffee combinations dynamically without creating a separate subclass for every combination.
28. Common Interview Questions
Which category does the Decorator Pattern belong to?
The Decorator Pattern is a:
Structural Design Pattern
What is the main purpose of the Decorator Pattern?
Its purpose is to add new responsibilities to an object dynamically without changing its original class.
Why does a decorator implement the same interface?
It implements the same interface so that:
It can replace the original object.
It can wrap other decorators.
Multiple decorators can be chained.
The client does not need to know whether it is using a basic object or a decorated object.
What relationships are present in a decorator?
A decorator has both:
IS-A component
HAS-A component
Which SOLID principles does it support?
It mainly supports:
Open/Closed Principle
Single Responsibility Principle
What principle is commonly associated with the Decorator Pattern?
Composition over inheritance
Can decorators be added at runtime?
Yes. Decorators can be added dynamically based on user choices or program conditions.
What is the main disadvantage of the pattern?
It may create many small wrapper objects and make the call flow difficult to debug.
29. The Heart of the Decorator Pattern
The most important part of the pattern is this type of method:
@Override
public int getCost() {
return coffee.getCost() + 20;
}
The decorator:
Calls the wrapped object's method.
Receives the existing result.
Adds its own behaviour.
Returns the updated result.
The general formula is:
Decorator Result
=
Wrapped Object Result
+
Additional Behaviour
In the coffee example:
Milk Decorator Cost
=
Existing Coffee Cost
+
Milk Cost
30. Final Summary
The Decorator Design Pattern allows us to add extra responsibilities to an object by wrapping it inside another object.
The original class remains unchanged.
Each decorator:
Implements the same interface.
Stores a reference to another object of that interface.
Delegates work to the wrapped object.
Adds its own additional behaviour.
The wrapping structure may look like this:
CreamDecorator
└── SugarDecorator
└── MilkDecorator
└── PlainCoffee
The key idea can be summarized in one sentence:
Wrap an object inside another object that follows the same interface, call the wrapped object's method, and add extra behaviour before or after that call.
The Decorator Pattern is especially useful when a system has many optional features and those features need to be combined dynamically without creating a large number of subclasses.