Decorator Design Pattern in Java: Complete Guide with Examples
Learn the Decorator design pattern in Java with simple explanations and practical examples. This guide covers how to add new behavior to objects dynamically using wrappers, with before-and-after code for beginners and experts.
I've been writing Java for years, and Decorator is the pattern I reach for
whenever I see an inheritance explosion. Not because inheritance is bad—because
the moment you create MilkCoffee, SugarCoffee, and
MilkSugarCoffee, you know you'll regret it. Here's how Decorator
saved me from that trap.
Why the Decorator Pattern Still Matters in Java
You've built a class. It works. Then requirements arrive: add logging, add caching, add
encryption, add compression. You start creating subclasses for every combination. Before
you know it, you have LoggingCachingEncryptedFileWriter and the class name
alone fills your screen.
The Decorator pattern solves this by letting you wrap an object with another object that has the same interface. The wrapper adds behavior before or after delegating to the original. You can stack wrappers as many times as you need, in any order, at runtime.
Whether you're building file streams, UI components, or HTTP clients, Decorator keeps your code flexible without the inheritance nightmare.
What is the Decorator Design Pattern?
"Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality."
In plain words: Decorator wraps an object with another object that has the same interface. The wrapper holds a reference to the original and adds behavior around the call. Since both share the interface, clients don't know they're talking to a wrapper.
The Four Players
| Role | Responsibility | Example |
|---|---|---|
| Component | Common interface for original and wrappers | Coffee |
| Concrete Component | The original object being wrapped | SimpleCoffee |
| Decorator | Abstract class that holds a reference to Component | CoffeeDecorator |
| Concrete Decorator | Adds specific behavior | MilkDecorator, SugarDecorator |
Decorator pattern structure: MilkDecorator wraps a Coffee, adds cost, and delegates to the original.
How to read this diagram (for freshers): The blue box at the top is the
Component—the common interface. On the left, SimpleCoffee
is the original object (Concrete Component). On the right, CoffeeDecorator
is the abstract wrapper that holds a reference to a Coffee (that's the red
dashed arrow). The two yellow boxes at the bottom are concrete wrappers. When you call
cost() on a MilkDecorator, it calls the original coffee's
cost() first, then adds 20. You can wrap a MilkDecorator inside
a SugarDecorator inside another MilkDecorator—stacking behavior
at runtime.
Real-World Example for Freshers
Before we touch code, let's look at the Decorator pattern you already use every day.
Example 1: Ordering Coffee at a Cafe
You walk into a coffee shop. You order a basic coffee. Then you add milk. Then you add sugar. Then you add whipped cream.
The barista doesn't create a new "MilkSugarWhippedCreamCoffee" object. The barista takes your basic coffee and wraps it with each addition, one by one. Each topping adds to the price. The final cup is still a coffee—but it's been decorated.
That's the Decorator pattern. The base object stays the same. The wrappers add behavior (and cost) on top.
Example 2: Java I/O Streams (You Already Use This)
Every time you write this line of Java, you're using the Decorator pattern:
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")));
Let's break it down:
FileInputStreamreads raw bytes from a file (the original object)InputStreamReaderwraps it and converts bytes to characters (a decorator)BufferedReaderwraps that and adds buffering plusreadLine()(another decorator)
All three share the Reader interface. You can stack them in any order. You
can add a LineNumberReader on top if you want line numbers. That's Decorator
in the standard library.
Example 3: Pizza Toppings
A pizza base is your Concrete Component. Cheese, mushrooms, olives, and pepperoni are
decorators. You don't create PizzaWithCheeseAndMushroomsAndOlives. You start
with a base pizza and wrap it with each topping. Each topping adds to the price and
modifies the description.
Quick Summary for Freshers
| Real-World Situation | Original Object | Decorators |
|---|---|---|
| Coffee shop | SimpleCoffee | Milk, Sugar, Whip |
| Pizza | Base Pizza | Cheese, Olives, Mushrooms |
| Java I/O | FileInputStream | InputStreamReader, BufferedReader |
| HTTP client | BasicClient | LoggingClient, RetryClient, AuthClient |
Once you see it in your coffee order, you'll see it everywhere. That's why Decorator is one of the most practical design patterns in everyday software.
Violation: The Class Explosion Nightmare
Imagine you're building the coffee shop app. You have a Coffee class. You
want to support milk, sugar, and whipped cream. With inheritance, you create:
class Coffee { }
class MilkCoffee extends Coffee { }
class SugarCoffee extends Coffee { }
class WhipCoffee extends Coffee { }
class MilkSugarCoffee extends Coffee { }
class MilkWhipCoffee extends Coffee { }
class SugarWhipCoffee extends Coffee { }
class MilkSugarWhipCoffee extends Coffee { }
// Add caramel? Eight more classes.
// Add vanilla? Sixteen more classes.
The Problem: Every new topping doubles the class count. This is the classic "class explosion" that inheritance causes when you have combinations of features. Decorator eliminates this entirely.
Solution: Decorator with Coffee and Toppings
Let's build a coffee ordering system where toppings are wrappers, not subclasses. The
client calls cost() once, and the wrappers add their prices automatically.
Step 1: The Component Interface
public interface Coffee {
double cost();
String description();
}
Step 2: The Concrete Component (Original Coffee)
public class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 50.0;
}
@Override
public String description() {
return "Simple Coffee";
}
}
Step 3: The Abstract Decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String description() {
return decoratedCoffee.description();
}
}
Step 4: The Concrete Decorators
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 20.0;
}
@Override
public String description() {
return super.description() + ", Milk";
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 10.0;
}
@Override
public String description() {
return super.description() + ", Sugar";
}
}
Step 5: Using the Decorator
public class Demo {
public static void main(String[] args) {
// Plain coffee
Coffee plain = new SimpleCoffee();
System.out.println(plain.description() + " = Rs." + plain.cost());
// Coffee with milk
Coffee milk = new MilkDecorator(new SimpleCoffee());
System.out.println(milk.description() + " = Rs." + milk.cost());
// Coffee with milk and sugar
Coffee milkSugar = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(milkSugar.description() + " = Rs." + milkSugar.cost());
// Coffee with milk, sugar, and extra milk
Coffee doubleMilk = new MilkDecorator(
new SugarDecorator(
new MilkDecorator(new SimpleCoffee())
)
);
System.out.println(doubleMilk.description() + " = Rs." + doubleMilk.cost());
}
}
Output:
Simple Coffee = Rs.50.0
Simple Coffee, Milk = Rs.70.0
Simple Coffee, Milk, Sugar = Rs.80.0
Simple Coffee, Milk, Sugar, Milk = Rs.100.0
What just happened? The client called cost() on the outermost
wrapper. That wrapper called its inner coffee's cost(), added its own price,
and returned. The chain unwound automatically. The client never wrote a single
if or instanceof.
For Beginners: The Gift Wrap Analogy
Think of buying a gift. You start with a book (the original object). You wrap it in bubble wrap (a decorator). Then you wrap that in gift paper (another decorator). Then you put it in a gift bag (a third decorator). The final package is still "a book"—but it's been decorated. Each layer adds something. You can add layers in any order. You can add as many as you want.
Decorator is the gift wrap. Inheritance would be creating a "BubbleWrappedBook" class, a "GiftPaperBook" class, and a "BubbleWrappedGiftPaperBook" class.
For Experts: Decorator vs. Composite (Know the Difference)
This is a common interview trap.
| Aspect | Decorator | Composite |
|---|---|---|
| Structure | Chain (one wrapped child) | Tree (many children) |
| Intent | Augment behavior | Aggregate part-whole |
| Children | Exactly 1 | 0 to many |
| Focus | Behavior | Structure |
Rule of thumb: Decorator is about one thing wearing many hats. Composite is about many things acting as one.
Putting It All Together: Java I/O Example
The most famous real-world Decorator in Java is java.io. Every stream wrapper
is a decorator. Here's the classic chain:
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt")
)
);
The Decorator Structure
| Role | Java Class |
|---|---|
| Component | Reader |
| Concrete Component | FileInputStream (wrapped by InputStreamReader) |
| Decorator | FilterReader |
| Concrete Decorators | BufferedReader, LineNumberReader |
BufferedReader adds buffering and readLine() to whatever
Reader it wraps. You can wrap it around a file reader, a socket reader, or a
string reader. The decorator doesn't care about the underlying source.
Personal note: I once had to add logging and retry logic to an HTTP
client. My first instinct was to create subclasses. Then I realized I'd need
LoggingClient, RetryClient, and
LoggingRetryClient—and if I added auth, that was eight classes. I refactored
to Decorator. The base client stayed untouched. Adding a feature meant writing one small
wrapper class.
Frequently Asked Questions
What is the Decorator design pattern in Java?
Decorator is a structural pattern that lets you attach new behaviors to objects dynamically by placing them inside wrapper objects. The wrapper has the same interface as the original object, so clients see no difference.
When should I use the Decorator pattern?
Use Decorator when you need to add responsibilities to individual objects without affecting others, when subclassing would cause a class explosion, or when behaviors can be combined in many ways at runtime.
How is Decorator different from Inheritance?
Inheritance adds behavior at compile time and affects all instances of a class. Decorator adds behavior at runtime to specific objects. Inheritance creates an is-a relationship; Decorator creates a has-a (wraps) relationship.
How is Decorator different from Composite?
Composite aggregates many children into a tree and delegates to all of them. Decorator wraps exactly one object and adds behavior around it. Composite is about structure; Decorator is about augmenting behavior.
Where is Decorator used in real Java code?
Java I/O is the classic example. BufferedReader wraps
InputStreamReader which wraps FileInputStream. All share the
Reader interface, so you can stack them.
Collections.synchronizedList() and
Collections.unmodifiableList() are also decorators.
Your Decorator Checklist
Before creating another subclass, ask:
- Am I multiplying classes? If you have
XWithA,XWithB,XWithAAndB—stop and consider Decorator. - Should behavior be added at runtime? If the combination of features isn't known until the user configures it, Decorator is right.
- Can I stack features? If adding feature A and feature B should work together, Decorator supports that naturally.
- Am I wrapping an existing object? If you have an object that needs extra behavior without changing its class, Decorator is the answer.
Start small. Refactor one exploding hierarchy. The clarity compounds.
Conclusion
The Decorator pattern isn't about complexity—it's about flexibility. When you need to add behavior to individual objects without touching their class, Decorator gives you a wrapper that stacks cleanly. It trades a small upfront design decision for long-term freedom from class explosion.
I'll be honest: I don't use Decorator in every project. Simple objects with fixed
behavior don't need it. But for anything that touches streams, HTTP clients, UI
components, or configurable features, it has saved me from writing the same subclass
combinations over and over. Start with the problem, not the pattern. If you find yourself
writing XWithAAndB, Decorator is probably the answer.
Related topics: SOLID Principles in Java, Composite Design Pattern, Bridge Design Pattern, Java I/O Streams, Structural Design Patterns
No comments:
Post a Comment