Description / Meta Description
Learn the Decorator Design Pattern in Java with practical examples. Understand how to add functionality dynamically without modifying existing classes. Discover real-world examples from Java I/O, Spring Framework, and enterprise applications.
Decorator Design Pattern in Java: Adding New Behavior Without Modifying Existing Code
In the previous article, we explored the Adapter Pattern, which helps incompatible systems work together.
Adapter solves:
“How do I make two incompatible interfaces communicate?”
Decorator solves a completely different problem:
“How do I add new functionality to an object without modifying its existing code?”
This is one of the most elegant and widely used Structural Design Patterns.
In fact, you’ve probably used Decorator hundreds of times without realizing it.
The Problem: Feature Explosion
Suppose you’re building a notification system.
Basic notification:
public interface Notification {
void send();
}
Implementation:
public class EmailNotification
implements Notification {
@Override
public void send() {
System.out.println(
"Sending Email");
}
}
Business is happy.
For one week.
Then requirements arrive:
- Add Logging
- Add Encryption
- Add Audit Trail
- Add Retry Logic
- Add Compression
Many developers immediately think:
EmailNotification
↓
LoggedEmailNotification
↓
EncryptedLoggedEmailNotification
↓
EncryptedLoggedRetriableEmailNotification
Class explosion begins.
Why Inheritance Becomes a Problem
Imagine:
Email Notification
SMS Notification
Push Notification
Now each needs:
Logging
Retry
Audit
Compression
Encryption
Potential combinations:
Email + Logging
Email + Retry
Email + Logging + Retry
SMS + Logging
SMS + Retry
SMS + Logging + Retry
Push + Logging
Push + Retry
Push + Logging + Retry
Number of classes grows exponentially.
Maintenance becomes a nightmare.
What We Really Want
Instead of creating:
LoggedEncryptedRetriableEmailNotification
we want:
EmailNotification
│
▼
LoggingDecorator
│
▼
EncryptionDecorator
│
▼
RetryDecorator
Each behavior remains independent.
This is exactly what Decorator provides.
What is Decorator Pattern?
Decorator is a Structural Design Pattern that:
Adds new behavior to an object dynamically without changing its existing code.
Think of it like:
Coffee
│
Milk
│
Sugar
│
Whipped Cream
You start with a basic coffee.
You decorate it with additional features.
The original coffee remains unchanged.
Architecture Diagram
Client
│
▼
Notification
▲
│
EmailNotification
▲
│
LoggingDecorator
▲
│
EncryptionDecorator
Each decorator wraps another object.
Step 1: Define Common Interface
public interface Notification {
void send();
}
Step 2: Concrete Component
public class EmailNotification
implements Notification {
@Override
public void send() {
System.out.println(
"Sending Email");
}
}
Step 3: Base Decorator
public abstract class NotificationDecorator
implements Notification {
protected Notification notification;
public NotificationDecorator(
Notification notification) {
this.notification = notification;
}
@Override
public void send() {
notification.send();
}
}
Notice:
Decorator HAS-A Notification
This is composition.
Not inheritance.
Step 4: Logging Decorator
public class LoggingDecorator
extends NotificationDecorator {
public LoggingDecorator(
Notification notification) {
super(notification);
}
@Override
public void send() {
System.out.println(
"Logging Request");
super.send();
}
}
Step 5: Encryption Decorator
public class EncryptionDecorator
extends NotificationDecorator {
public EncryptionDecorator(
Notification notification) {
super(notification);
}
@Override
public void send() {
System.out.println(
"Encrypting Message");
super.send();
}
}
Step 6: Client Usage
Notification notification =
new EncryptionDecorator(
new LoggingDecorator(
new EmailNotification()));
notification.send();
Output:
Encrypting Message
Logging Request
Sending Email
Notice:
No existing code changed.
New behavior added dynamically.
How Decorator Works Internally
Execution flow:
Client
│
▼
EncryptionDecorator
│
▼
LoggingDecorator
│
▼
EmailNotification
Each layer adds functionality.
Then delegates to the wrapped object.
Real-World Example: Coffee Shop
Classic Decorator example.
Base coffee:
Coffee coffee =
new BasicCoffee();
Add milk:
coffee =
new MilkDecorator(coffee);
Add sugar:
coffee =
new SugarDecorator(coffee);
Add whipped cream:
coffee =
new WhippedCreamDecorator(coffee);
Result:
Basic Coffee
+ Milk
+ Sugar
+ Whipped Cream
The original coffee class never changes.
Java I/O: The Most Famous Decorator Example
Most Java developers have already used Decorator.
Example:
InputStream input =
new FileInputStream("data.txt");
Now decorate:
InputStream buffered =
new BufferedInputStream(input);
Further decorate:
DataInputStream dataInput =
new DataInputStream(buffered);
Architecture:
FileInputStream
│
BufferedInputStream
│
DataInputStream
Each layer adds functionality.
This is textbook Decorator Pattern.
Spring Framework Examples
Decorator appears throughout Spring.
HttpServletRequest Wrappers
HttpServletRequest
Decorated with:
Security
Caching
Logging
Auditing
without changing original request objects.
Spring Security Filters
Chain processing often behaves similarly:
Authentication
↓
Authorization
↓
Audit
↓
Request Processing
Each layer adds behavior.
Decorator vs Inheritance
This is the key design principle.
Inheritance Approach
EmailNotification
│
▼
LoggedEmailNotification
│
▼
EncryptedLoggedEmailNotification
Problems:
- rigid hierarchy
- class explosion
- difficult maintenance
Decorator Approach
EmailNotification
│
▼
LoggingDecorator
│
▼
EncryptionDecorator
Benefits:
- reusable
- flexible
- dynamic
Decorator vs Adapter
A common interview question.
Adapter
Purpose:
Change Interface
Example:
pay()
↓
makePayment()
Decorator
Purpose:
Keep Interface
Add Behavior
Example:
send()
↓
Logged send()
↓
Encrypted send()
Decorator vs Proxy
Another common confusion.
Decorator
Goal:
Enhance Functionality
Proxy
Goal:
Control Access
Examples:
Security Checks
Lazy Loading
Remote Calls
Caching
Proxy protects.
Decorator enhances.
Benefits of Decorator Pattern
1. Open/Closed Principle
Add behavior.
Avoid modifying existing classes.
2. Flexible Composition
Mix and match features.
3. Avoid Class Explosion
No endless inheritance trees.
4. Reusable Components
Decorators work across multiple objects.
5. Runtime Configuration
Behavior can be assembled dynamically.
Common Mistakes
Mistake 1: Too Many Decorator Layers
Example:
Decorator
Decorator
Decorator
Decorator
Decorator
Debugging becomes harder.
Mistake 2: Business Logic Leakage
Decorators should enhance behavior.
Not contain core business workflows.
Mistake 3: Confusing Decorator with Inheritance
If behavior is static and permanent, inheritance may be simpler.
Use Decorator when flexibility matters.
When Should You Use Decorator?
Use Decorator when:
✔ Functionality should be added dynamically
✔ Modifying existing code is undesirable
✔ Multiple feature combinations exist
✔ Inheritance becomes messy
✔ Open/Closed Principle is important
Avoid Decorator when:
❌ Behavior never changes
❌ Simpler inheritance works
❌ Extra abstraction adds little value
Quick Comparison
| Pattern | Purpose |
|---|---|
| Adapter | Make interfaces compatible |
| Decorator | Add behavior dynamically |
| Proxy | Control access |
| Facade | Simplify complexity |
Final Thoughts
The Decorator Pattern is one of the most elegant examples of:
“Favor Composition Over Inheritance.”
Instead of creating endless subclasses, Decorator allows functionality to be assembled like building blocks.
This is why Decorator appears heavily in:
- Java I/O
- Spring Framework
- Logging frameworks
- Security layers
- Enterprise middleware
Whenever you need to extend behavior without modifying existing code, Decorator should be one of the first patterns you consider.
In the next article, we’ll continue our Structural Design Pattern journey with:
Facade Pattern — Hiding Complex Systems Behind a Simple Interface
You’ll learn why frameworks like Spring’s JdbcTemplate, RestTemplate, and many enterprise APIs are classic examples of the Facade pattern.