Decorator / Wrapper Pattern
1. What is Decorator Pattern?
The Decorator pattern is a GOF Structural Pattern, also called the Wrapper Pattern. It attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
One line: Add new responsibilities/functionalities to an existing object as wrappers without affecting the existing object. Uses composition, not inheritance.
2. The Problem: Class Explosion via Inheritance
Using inheritance to add new responsibilities/functionalities on top of an existing object will just increase the number of classes and create huge problems for application users.
Ice cream example
An ice cream shop has:
- 3 basic flavours: Vanilla, ButterScotch, Strawberry
- 4 toppings: DryFruits, Honey, Cookies, ChocoChips
If we prepare classes by keeping all permutations and combinations in mind: potentially 3 x 4! = 72+ classes. If basic flavours count and toppings count is increasing, we will end up developing lots of classes satisfying all permutations and combinations.
// Problem: inheritance-based approach (class explosion!)
// Common Interface
package com.nt.comps;
public interface Icecream {
void prepare();
}
// Concrete class 1
package com.nt.comps;
public class ButterScotchIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("preparing ButterScotch iceCream");
}
}
// Concrete class 2
package com.nt.comps;
public class VanilaIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("preparing Vanilla iceCream");
}
}
// Extension via inheritance: DryFruit on Vanilla
package com.nt.comps;
public class DryFruitVanilaIcecream extends VanilaIcecream {
@Override
public void prepare() {
System.out.println("preparing Vanilla iceCream");
addDryFruits();
}
public void addDryFruits() {
System.out.println("Adding DryFruits.....");
}
}
// Extension via inheritance: Honey + DryFruit on Vanilla
package com.nt.comps;
public class HoneyDryFruitVanilaIcecream extends DryFruitVanilaIcecream {
@Override
public void prepare() {
super.prepare();
addHoney();
}
public void addHoney() {
System.out.println("Adding Honey.....");
}
}
// ... and many many more classes will come here .....
// For EVERY combination, we need a new class!
3. The Solution: Decorators as Wrappers
Instead of creating a new class for each combination, wrap the base object with decorator objects. Each decorator adds one responsibility. Decorators can be stacked in any order to create any combination dynamically at runtime.
Four Components
| Component | Role | Ice Cream Example |
|---|---|---|
| a) Component Interface | The common interface implemented by all concrete classes and decorators to make a single type | Icecream |
| b) Concrete Components | Classes implementing Component Interface providing basic objects on which we may need to add additional functionalities | VanilaIcecream, ButterScotchIcecream, StrawberryIcecream |
| c) Abstract Decorator | The common and abstract super class for all Decorators. Implements Component Interface AND also maintains same interface type variable as the HAS-A property. | IceCreamDecorator |
| d) Concrete Decorators | Extends from Abstract Decorator having logics to provide additional functionalities for the Concrete Component classes objects. | DryFruitDecorator, HoneyDecorator, ChocoChipsDecorator |
4. Ice Cream Example (Complete Code)
Component Interface
package com.nt.comps;
public interface Icecream {
void prepare();
}
Concrete Components
package com.nt.comps;
public class VanilaIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("Preparing Vanilla Icecream");
}
}
public class ButterScotchIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("Preparing ButterScotch Icecream");
}
}
public class StrawberryIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("Preparing Strawberry Icecream");
}
}
Abstract Decorator
package com.nt.decorators;
import com.nt.comps.Icecream;
public abstract class IceCreamDecorator implements Icecream {
protected Icecream icecream; // HAS-A (wraps another Icecream)
public IceCreamDecorator(Icecream icecream) {
this.icecream = icecream;
}
@Override
public void prepare() {
icecream.prepare(); // delegate to the wrapped component
}
}
Concrete Decorators
package com.nt.decorators;
import com.nt.comps.Icecream;
public class DryFruitDecorator extends IceCreamDecorator {
public DryFruitDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addDryFruits();
}
private void addDryFruits() {
System.out.println(" + Adding Dry Fruits");
}
}
public class HoneyDecorator extends IceCreamDecorator {
public HoneyDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addHoney();
}
private void addHoney() {
System.out.println(" + Adding Honey");
}
}
public class ChocoChipsDecorator extends IceCreamDecorator {
public ChocoChipsDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addChocoChips();
}
private void addChocoChips() {
System.out.println(" + Adding Choco Chips");
}
}
public class CookiesDecorator extends IceCreamDecorator {
public CookiesDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addCookies();
}
private void addCookies() {
System.out.println(" + Adding Cookies");
}
}
Client test
package com.nt.test;
import com.nt.comps.*;
import com.nt.decorators.*;
public class IceCreamTest {
public static void main(String[] args) {
// Customer 1: DryFruit Vanilla IceCream
System.out.println("=== Customer 1 ===");
Icecream c1 = new DryFruitDecorator(new VanilaIcecream());
c1.prepare();
System.out.println();
// Customer 2: DryFruit + Honey + Cookies ButterScotch
System.out.println("=== Customer 2 ===");
Icecream c2 = new DryFruitDecorator(
new HoneyDecorator(
new CookiesDecorator(
new ButterScotchIcecream())));
c2.prepare();
System.out.println();
// Customer 3: ChocoChips Strawberry
System.out.println("=== Customer 3 ===");
Icecream c3 = new ChocoChipsDecorator(new StrawberryIcecream());
c3.prepare();
}
}
5. Dosa Example (UseCase 2 from course)
| Component | Classes |
|---|---|
| Component Interface | Dosa |
| Concrete Components | GreenGramDosa, BlackGramDosa, RagiDosa, MilletDosa |
| Abstract Decorator | BaseDosaDecorator |
| Concrete Decorators | PaneerDosaDecorator, MasalaDosaDecorator, MixVegDosaDecorator, EggDosaDecorator |
// Component Interface
package com.nt.dosa;
public interface Dosa {
void prepare();
double cost();
}
// Concrete Component
public class GreenGramDosa implements Dosa {
public void prepare() {
System.out.println("Preparing GreenGram Dosa (base)");
}
public double cost() { return 50.0; }
}
public class BlackGramDosa implements Dosa {
public void prepare() {
System.out.println("Preparing BlackGram Dosa (base)");
}
public double cost() { return 40.0; }
}
// Abstract Decorator
public abstract class BaseDosaDecorator implements Dosa {
protected Dosa dosa;
public BaseDosaDecorator(Dosa dosa) { this.dosa = dosa; }
public void prepare() { dosa.prepare(); }
public double cost() { return dosa.cost(); }
}
// Concrete Decorators
public class MasalaDosaDecorator extends BaseDosaDecorator {
public MasalaDosaDecorator(Dosa dosa) { super(dosa); }
public void prepare() {
super.prepare();
System.out.println(" + Adding Masala filling");
}
public double cost() { return super.cost() + 20.0; }
}
public class PaneerDosaDecorator extends BaseDosaDecorator {
public PaneerDosaDecorator(Dosa dosa) { super(dosa); }
public void prepare() {
super.prepare();
System.out.println(" + Adding Paneer topping");
}
public double cost() { return super.cost() + 30.0; }
}
public class EggDosaDecorator extends BaseDosaDecorator {
public EggDosaDecorator(Dosa dosa) { super(dosa); }
public void prepare() {
super.prepare();
System.out.println(" + Adding Egg layer");
}
public double cost() { return super.cost() + 15.0; }
}
// Client
Dosa order = new PaneerDosaDecorator(new MasalaDosaDecorator(new GreenGramDosa()));
order.prepare();
System.out.println("Total cost: Rs " + order.cost());
6. Pre-defined Decorator Examples in Java
The entire java.io stream hierarchy is built on the Decorator pattern. Base streams (FileInputStream, FileOutputStream) are wrapped with decorators (BufferedInputStream, DataInputStream, ObjectInputStream) to add functionality.
// Decorator pattern in JDK I/O:
DataInputStream dis = new DataInputStream(
new FileInputStream("abc.txt"));
// FileInputStream = concrete component (reads raw bytes)
// DataInputStream = decorator (adds readInt(), readDouble(), etc.)
DataOutputStream dos = new DataOutputStream(
new FileOutputStream("abc.txt"));
// FileOutputStream = concrete component (writes raw bytes)
// DataOutputStream = decorator (adds writeInt(), writeDouble(), etc.)
Scanner scanner = new Scanner(System.in);
// System.in = concrete component (raw byte stream)
// Scanner = decorator (adds nextInt(), nextLine(), etc.)
| Wrapper Class (Decorator) | Wraps | Adds |
|---|---|---|
BufferedReader | Reader | Buffering for efficient reading, readLine() |
BufferedWriter | Writer | Buffering for efficient writing |
DataInputStream | InputStream | readInt(), readDouble(), readUTF() |
DataOutputStream | OutputStream | writeInt(), writeDouble(), writeUTF() |
ObjectInputStream | InputStream | readObject() for deserialization |
ObjectOutputStream | OutputStream | writeObject() for serialization |
PrintStream | OutputStream | print(), println(), printf() |
InputStreamReader | InputStream | Byte-to-character bridge (also Adapter) |
7. Full-Length Industry Project: Logging Pipeline with Decorators
National Bank of India needs a flexible logging system. The base logger writes plain text to a file. Different services need different enhancements: some need timestamps, some need log levels (INFO, ERROR), some need the caller class name, and some need all three. Instead of creating TimestampLogger, LevelLogger, TimestampLevelLogger, TimestampLevelCallerLogger (class explosion), decorators can be stacked as needed.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
Logger.java (Component Interface)
package com.nbi.logging;
public interface Logger {
void log(String message);
void close();
}
FileLogger.java (Concrete Component)
package com.nbi.logging;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
public class FileLogger implements Logger {
private final PrintWriter writer;
private final List<String> logHistory = new ArrayList<>();
public FileLogger(String filePath) {
try {
this.writer = new PrintWriter(new FileWriter(filePath, true), true);
} catch (IOException e) {
throw new RuntimeException("Cannot open log file: " + filePath, e);
}
}
@Override
public void log(String message) {
writer.println(message);
System.out.println(message);
logHistory.add(message);
}
@Override
public void close() {
writer.close();
}
public List<String> getLogHistory() {
return logHistory;
}
}
LoggerDecorator.java (Abstract Decorator)
package com.nbi.logging;
public abstract class LoggerDecorator implements Logger {
protected final Logger wrapped; // HAS-A (wraps another Logger)
protected LoggerDecorator(Logger wrapped) {
this.wrapped = wrapped;
}
@Override
public void log(String message) {
wrapped.log(message); // delegate to wrapped logger
}
@Override
public void close() {
wrapped.close();
}
}
TimestampDecorator.java
package com.nbi.logging;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TimestampDecorator extends LoggerDecorator {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public TimestampDecorator(Logger wrapped) {
super(wrapped);
}
@Override
public void log(String message) {
String timestamp = LocalDateTime.now().format(FORMATTER);
String decorated = "[" + timestamp + "] " + message;
super.log(decorated);
}
}
LevelDecorator.java
package com.nbi.logging;
public class LevelDecorator extends LoggerDecorator {
private final String level;
public LevelDecorator(Logger wrapped, String level) {
super(wrapped);
this.level = level;
}
@Override
public void log(String message) {
String decorated = "[" + level + "] " + message;
super.log(decorated);
}
}
CallerDecorator.java
package com.nbi.logging;
public class CallerDecorator extends LoggerDecorator {
private final String callerClass;
public CallerDecorator(Logger wrapped, String callerClass) {
super(wrapped);
this.callerClass = callerClass;
}
@Override
public void log(String message) {
String decorated = "[" + callerClass + "] " + message;
super.log(decorated);
}
}
EncryptionDecorator.java
package com.nbi.logging;
import java.util.Base64;
public class EncryptionDecorator extends LoggerDecorator {
public EncryptionDecorator(Logger wrapped) {
super(wrapped);
}
@Override
public void log(String message) {
// Simple Base64 encoding to demonstrate concept
// In production, use AES or similar encryption
String encrypted = Base64.getEncoder().encodeToString(message.getBytes());
String decorated = "[ENCRYPTED] " + encrypted;
super.log(decorated);
}
}
Application.java
package com.nbi.logging;
public class Application {
public static void main(String[] args) {
// Scenario 1: Plain file logger (no decorators)
System.out.println("=== Scenario 1: Plain Logger ===");
Logger plain = new FileLogger("plain.log");
plain.log("Application started");
plain.log("Processing request");
plain.close();
System.out.println();
// Scenario 2: Timestamp + Level (AccountService style)
System.out.println("=== Scenario 2: Timestamp + Level ===");
Logger accountLogger = new TimestampDecorator(
new LevelDecorator(
new FileLogger("account.log"), "INFO"));
accountLogger.log("Account SAV2001 balance queried");
accountLogger.log("Balance: Rs 250000");
accountLogger.close();
System.out.println();
// Scenario 3: Timestamp + Level + Caller (TransferService style)
System.out.println("=== Scenario 3: Timestamp + Level + Caller ===");
Logger transferLogger = new TimestampDecorator(
new LevelDecorator(
new CallerDecorator(
new FileLogger("transfer.log"),
"TransferService"), "INFO"));
transferLogger.log("Transfer initiated: SAV2001 -> CUR3001");
transferLogger.log("Amount: Rs 50000");
transferLogger.close();
System.out.println();
// Scenario 4: All decorators including encryption (AuditService style)
System.out.println("=== Scenario 4: All + Encryption ===");
Logger auditLogger = new TimestampDecorator(
new LevelDecorator(
new CallerDecorator(
new EncryptionDecorator(
new FileLogger("audit.log")),
"AuditService"), "AUDIT"));
auditLogger.log("Sensitive: Customer PAN ABCDE1234F accessed");
auditLogger.close();
System.out.println();
// Scenario 5: Different level on same base
System.out.println("=== Scenario 5: ERROR level ===");
Logger errorLogger = new TimestampDecorator(
new LevelDecorator(
new FileLogger("error.log"), "ERROR"));
errorLogger.log("Database connection failed");
errorLogger.close();
}
}
Expected Output
Key observations from the project
- No class explosion: 4 decorators + 1 base logger = 5 classes. Without decorator, we would need: PlainLogger, TimestampLogger, LevelLogger, CallerLogger, TimestampLevelLogger, TimestampCallerLogger, LevelCallerLogger, TimestampLevelCallerLogger, plus encryption variants = 16+ classes.
- Stack any combination: Each service creates its own pipeline by stacking decorators. AccountService uses Timestamp+Level. TransferService adds Caller. AuditService adds Encryption. No new classes needed.
- Order matters: Decorators wrap from inside out. The innermost decorator runs first. In scenario 3, CallerDecorator adds class name, then LevelDecorator adds level, then TimestampDecorator adds timestamp.
- Same interface throughout: FileLogger, TimestampDecorator, LevelDecorator all implement
Logger. The client works withLoggertype regardless of how many decorators are stacked. - Open/Closed Principle: Adding a new decorator (e.g., ThreadNameDecorator) requires only one new class. No existing code changes.
8. When to Use and When NOT to Use
When to use Decorator
| Scenario | Example | Why Decorator |
|---|---|---|
| Dynamic feature addition | Adding toppings to ice cream at runtime | Customer chooses features dynamically, not at compile time |
| I/O stream wrapping | BufferedReader wrapping FileReader | Add buffering, data types, encryption to any stream |
| Logging enhancements | Timestamp, level, caller decoration | Different services need different log formats |
| UI component decoration | Adding scrollbar, border, shadow to a window | Combine visual features without subclassing |
| Middleware pipeline | Authentication, logging, compression in HTTP | Stack filters in any order |
When NOT to use Decorator
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Fixed combinations | If combinations are known and few, decorator adds complexity | Simple subclassing |
| Identity matters | Decorated object has different identity (instanceof fails) | Inheritance |
| Too many small decorators | Debugging a chain of 10 decorators is hard | Builder or configuration object |
Decorator vs Inheritance
| Aspect | Decorator | Inheritance |
|---|---|---|
| Mechanism | Composition (HAS-A) | IS-A relationship |
| When | Runtime (dynamic) | Compile time (static) |
| Combinations | Any order, any count | One fixed hierarchy |
| Class count | N components + M decorators | N x M combination classes |
| Open/Closed | Yes (add new decorator) | Partial (add subclass, but hierarchy grows) |
9. Quick Reference Card
| Component | Role | In Logging Project | In Ice Cream |
|---|---|---|---|
| Component Interface | Common type for all | Logger | Icecream |
| Concrete Component | Base object | FileLogger | VanilaIcecream |
| Abstract Decorator | Wraps + delegates | LoggerDecorator | IceCreamDecorator |
| Concrete Decorator | Adds one feature | TimestampDecorator | DryFruitDecorator |
Decorator / Wrapper Design Pattern in Java
1. What is Decorator Pattern?
The Decorator pattern is a GOF Structural Pattern, also called the Wrapper Pattern. It attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
One line: Add new responsibilities/functionalities to an existing object as wrappers without affecting the existing object. Uses composition, not inheritance.
2. The Problem: Class Explosion via Inheritance
Using inheritance to add new responsibilities/functionalities on top of an existing object will just increase the number of classes and create huge problems for application users.
Ice cream example
An ice cream shop has:
- 3 basic flavours: Vanilla, ButterScotch, Strawberry
- 4 toppings: DryFruits, Honey, Cookies, ChocoChips
If we prepare classes by keeping all permutations and combinations in mind: potentially 3 x 4! = 72+ classes. If basic flavours count and toppings count is increasing, we will end up developing lots of classes satisfying all permutations and combinations.
// Problem: inheritance-based approach (class explosion!)
// Common Interface
package com.nt.comps;
public interface Icecream {
void prepare();
}
// Concrete class 1
package com.nt.comps;
public class ButterScotchIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("preparing ButterScotch iceCream");
}
}
// Concrete class 2
package com.nt.comps;
public class VanilaIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("preparing Vanilla iceCream");
}
}
// Extension via inheritance: DryFruit on Vanilla
package com.nt.comps;
public class DryFruitVanilaIcecream extends VanilaIcecream {
@Override
public void prepare() {
System.out.println("preparing Vanilla iceCream");
addDryFruits();
}
public void addDryFruits() {
System.out.println("Adding DryFruits.....");
}
}
// Extension via inheritance: Honey + DryFruit on Vanilla
package com.nt.comps;
public class HoneyDryFruitVanilaIcecream extends DryFruitVanilaIcecream {
@Override
public void prepare() {
super.prepare();
addHoney();
}
public void addHoney() {
System.out.println("Adding Honey.....");
}
}
// ... and many many more classes will come here .....
// For EVERY combination, we need a new class!
3. The Solution: Decorators as Wrappers
Instead of creating a new class for each combination, wrap the base object with decorator objects. Each decorator adds one responsibility. Decorators can be stacked in any order to create any combination dynamically at runtime.
Four Components
| Component | Role | Ice Cream Example |
|---|---|---|
| a) Component Interface | The common interface implemented by all concrete classes and decorators to make a single type | Icecream |
| b) Concrete Components | Classes implementing Component Interface providing basic objects on which we may need to add additional functionalities | VanilaIcecream, ButterScotchIcecream, StrawberryIcecream |
| c) Abstract Decorator | The common and abstract super class for all Decorators. Implements Component Interface AND also maintains same interface type variable as the HAS-A property. | IceCreamDecorator |
| d) Concrete Decorators | Extends from Abstract Decorator having logics to provide additional functionalities for the Concrete Component classes objects. | DryFruitDecorator, HoneyDecorator, ChocoChipsDecorator |
4. Ice Cream Example (Complete Code)
Component Interface
package com.nt.comps;
public interface Icecream {
void prepare();
}
Concrete Components
package com.nt.comps;
public class VanilaIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("Preparing Vanilla Icecream");
}
}
public class ButterScotchIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("Preparing ButterScotch Icecream");
}
}
public class StrawberryIcecream implements Icecream {
@Override
public void prepare() {
System.out.println("Preparing Strawberry Icecream");
}
}
Abstract Decorator
package com.nt.decorators;
import com.nt.comps.Icecream;
public abstract class IceCreamDecorator implements Icecream {
protected Icecream icecream; // HAS-A (wraps another Icecream)
public IceCreamDecorator(Icecream icecream) {
this.icecream = icecream;
}
@Override
public void prepare() {
icecream.prepare(); // delegate to the wrapped component
}
}
Concrete Decorators
package com.nt.decorators;
import com.nt.comps.Icecream;
public class DryFruitDecorator extends IceCreamDecorator {
public DryFruitDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addDryFruits();
}
private void addDryFruits() {
System.out.println(" + Adding Dry Fruits");
}
}
public class HoneyDecorator extends IceCreamDecorator {
public HoneyDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addHoney();
}
private void addHoney() {
System.out.println(" + Adding Honey");
}
}
public class ChocoChipsDecorator extends IceCreamDecorator {
public ChocoChipsDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addChocoChips();
}
private void addChocoChips() {
System.out.println(" + Adding Choco Chips");
}
}
public class CookiesDecorator extends IceCreamDecorator {
public CookiesDecorator(Icecream icecream) {
super(icecream);
}
@Override
public void prepare() {
super.prepare();
addCookies();
}
private void addCookies() {
System.out.println(" + Adding Cookies");
}
}
Client test
package com.nt.test;
import com.nt.comps.*;
import com.nt.decorators.*;
public class IceCreamTest {
public static void main(String[] args) {
// Customer 1: DryFruit Vanilla IceCream
System.out.println("=== Customer 1 ===");
Icecream c1 = new DryFruitDecorator(new VanilaIcecream());
c1.prepare();
System.out.println();
// Customer 2: DryFruit + Honey + Cookies ButterScotch
System.out.println("=== Customer 2 ===");
Icecream c2 = new DryFruitDecorator(
new HoneyDecorator(
new CookiesDecorator(
new ButterScotchIcecream())));
c2.prepare();
System.out.println();
// Customer 3: ChocoChips Strawberry
System.out.println("=== Customer 3 ===");
Icecream c3 = new ChocoChipsDecorator(new StrawberryIcecream());
c3.prepare();
}
}
5. Dosa Example (UseCase 2 from course)
| Component | Classes |
|---|---|
| Component Interface | Dosa |
| Concrete Components | GreenGramDosa, BlackGramDosa, RagiDosa, MilletDosa |
| Abstract Decorator | BaseDosaDecorator |
| Concrete Decorators | PaneerDosaDecorator, MasalaDosaDecorator, MixVegDosaDecorator, EggDosaDecorator |
// Component Interface
package com.nt.dosa;
public interface Dosa {
void prepare();
double cost();
}
// Concrete Component
public class GreenGramDosa implements Dosa {
public void prepare() {
System.out.println("Preparing GreenGram Dosa (base)");
}
public double cost() { return 50.0; }
}
public class BlackGramDosa implements Dosa {
public void prepare() {
System.out.println("Preparing BlackGram Dosa (base)");
}
public double cost() { return 40.0; }
}
// Abstract Decorator
public abstract class BaseDosaDecorator implements Dosa {
protected Dosa dosa;
public BaseDosaDecorator(Dosa dosa) { this.dosa = dosa; }
public void prepare() { dosa.prepare(); }
public double cost() { return dosa.cost(); }
}
// Concrete Decorators
public class MasalaDosaDecorator extends BaseDosaDecorator {
public MasalaDosaDecorator(Dosa dosa) { super(dosa); }
public void prepare() {
super.prepare();
System.out.println(" + Adding Masala filling");
}
public double cost() { return super.cost() + 20.0; }
}
public class PaneerDosaDecorator extends BaseDosaDecorator {
public PaneerDosaDecorator(Dosa dosa) { super(dosa); }
public void prepare() {
super.prepare();
System.out.println(" + Adding Paneer topping");
}
public double cost() { return super.cost() + 30.0; }
}
public class EggDosaDecorator extends BaseDosaDecorator {
public EggDosaDecorator(Dosa dosa) { super(dosa); }
public void prepare() {
super.prepare();
System.out.println(" + Adding Egg layer");
}
public double cost() { return super.cost() + 15.0; }
}
// Client
Dosa order = new PaneerDosaDecorator(new MasalaDosaDecorator(new GreenGramDosa()));
order.prepare();
System.out.println("Total cost: Rs " + order.cost());
6. Pre-defined Decorator Examples in Java
The entire java.io stream hierarchy is built on the Decorator pattern. Base streams (FileInputStream, FileOutputStream) are wrapped with decorators (BufferedInputStream, DataInputStream, ObjectInputStream) to add functionality.
// Decorator pattern in JDK I/O:
DataInputStream dis = new DataInputStream(
new FileInputStream("abc.txt"));
// FileInputStream = concrete component (reads raw bytes)
// DataInputStream = decorator (adds readInt(), readDouble(), etc.)
DataOutputStream dos = new DataOutputStream(
new FileOutputStream("abc.txt"));
// FileOutputStream = concrete component (writes raw bytes)
// DataOutputStream = decorator (adds writeInt(), writeDouble(), etc.)
Scanner scanner = new Scanner(System.in);
// System.in = concrete component (raw byte stream)
// Scanner = decorator (adds nextInt(), nextLine(), etc.)
| Wrapper Class (Decorator) | Wraps | Adds |
|---|---|---|
BufferedReader | Reader | Buffering for efficient reading, readLine() |
BufferedWriter | Writer | Buffering for efficient writing |
DataInputStream | InputStream | readInt(), readDouble(), readUTF() |
DataOutputStream | OutputStream | writeInt(), writeDouble(), writeUTF() |
ObjectInputStream | InputStream | readObject() for deserialization |
ObjectOutputStream | OutputStream | writeObject() for serialization |
PrintStream | OutputStream | print(), println(), printf() |
InputStreamReader | InputStream | Byte-to-character bridge (also Adapter) |
7. Full-Length Industry Project: Logging Pipeline with Decorators
National Bank of India needs a flexible logging system. The base logger writes plain text to a file. Different services need different enhancements: some need timestamps, some need log levels (INFO, ERROR), some need the caller class name, and some need all three. Instead of creating TimestampLogger, LevelLogger, TimestampLevelLogger, TimestampLevelCallerLogger (class explosion), decorators can be stacked as needed.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
Logger.java (Component Interface)
package com.nbi.logging;
public interface Logger {
void log(String message);
void close();
}
FileLogger.java (Concrete Component)
package com.nbi.logging;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
public class FileLogger implements Logger {
private final PrintWriter writer;
private final List<String> logHistory = new ArrayList<>();
public FileLogger(String filePath) {
try {
this.writer = new PrintWriter(new FileWriter(filePath, true), true);
} catch (IOException e) {
throw new RuntimeException("Cannot open log file: " + filePath, e);
}
}
@Override
public void log(String message) {
writer.println(message);
System.out.println(message);
logHistory.add(message);
}
@Override
public void close() {
writer.close();
}
public List<String> getLogHistory() {
return logHistory;
}
}
LoggerDecorator.java (Abstract Decorator)
package com.nbi.logging;
public abstract class LoggerDecorator implements Logger {
protected final Logger wrapped; // HAS-A (wraps another Logger)
protected LoggerDecorator(Logger wrapped) {
this.wrapped = wrapped;
}
@Override
public void log(String message) {
wrapped.log(message); // delegate to wrapped logger
}
@Override
public void close() {
wrapped.close();
}
}
TimestampDecorator.java
package com.nbi.logging;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TimestampDecorator extends LoggerDecorator {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public TimestampDecorator(Logger wrapped) {
super(wrapped);
}
@Override
public void log(String message) {
String timestamp = LocalDateTime.now().format(FORMATTER);
String decorated = "[" + timestamp + "] " + message;
super.log(decorated);
}
}
LevelDecorator.java
package com.nbi.logging;
public class LevelDecorator extends LoggerDecorator {
private final String level;
public LevelDecorator(Logger wrapped, String level) {
super(wrapped);
this.level = level;
}
@Override
public void log(String message) {
String decorated = "[" + level + "] " + message;
super.log(decorated);
}
}
CallerDecorator.java
package com.nbi.logging;
public class CallerDecorator extends LoggerDecorator {
private final String callerClass;
public CallerDecorator(Logger wrapped, String callerClass) {
super(wrapped);
this.callerClass = callerClass;
}
@Override
public void log(String message) {
String decorated = "[" + callerClass + "] " + message;
super.log(decorated);
}
}
EncryptionDecorator.java
package com.nbi.logging;
import java.util.Base64;
public class EncryptionDecorator extends LoggerDecorator {
public EncryptionDecorator(Logger wrapped) {
super(wrapped);
}
@Override
public void log(String message) {
// Simple Base64 encoding to demonstrate concept
// In production, use AES or similar encryption
String encrypted = Base64.getEncoder().encodeToString(message.getBytes());
String decorated = "[ENCRYPTED] " + encrypted;
super.log(decorated);
}
}
Application.java
package com.nbi.logging;
public class Application {
public static void main(String[] args) {
// Scenario 1: Plain file logger (no decorators)
System.out.println("=== Scenario 1: Plain Logger ===");
Logger plain = new FileLogger("plain.log");
plain.log("Application started");
plain.log("Processing request");
plain.close();
System.out.println();
// Scenario 2: Timestamp + Level (AccountService style)
System.out.println("=== Scenario 2: Timestamp + Level ===");
Logger accountLogger = new TimestampDecorator(
new LevelDecorator(
new FileLogger("account.log"), "INFO"));
accountLogger.log("Account SAV2001 balance queried");
accountLogger.log("Balance: Rs 250000");
accountLogger.close();
System.out.println();
// Scenario 3: Timestamp + Level + Caller (TransferService style)
System.out.println("=== Scenario 3: Timestamp + Level + Caller ===");
Logger transferLogger = new TimestampDecorator(
new LevelDecorator(
new CallerDecorator(
new FileLogger("transfer.log"),
"TransferService"), "INFO"));
transferLogger.log("Transfer initiated: SAV2001 -> CUR3001");
transferLogger.log("Amount: Rs 50000");
transferLogger.close();
System.out.println();
// Scenario 4: All decorators including encryption (AuditService style)
System.out.println("=== Scenario 4: All + Encryption ===");
Logger auditLogger = new TimestampDecorator(
new LevelDecorator(
new CallerDecorator(
new EncryptionDecorator(
new FileLogger("audit.log")),
"AuditService"), "AUDIT"));
auditLogger.log("Sensitive: Customer PAN ABCDE1234F accessed");
auditLogger.close();
System.out.println();
// Scenario 5: Different level on same base
System.out.println("=== Scenario 5: ERROR level ===");
Logger errorLogger = new TimestampDecorator(
new LevelDecorator(
new FileLogger("error.log"), "ERROR"));
errorLogger.log("Database connection failed");
errorLogger.close();
}
}
Expected Output
Key observations from the project
- No class explosion: 4 decorators + 1 base logger = 5 classes. Without decorator, we would need: PlainLogger, TimestampLogger, LevelLogger, CallerLogger, TimestampLevelLogger, TimestampCallerLogger, LevelCallerLogger, TimestampLevelCallerLogger, plus encryption variants = 16+ classes.
- Stack any combination: Each service creates its own pipeline by stacking decorators. AccountService uses Timestamp+Level. TransferService adds Caller. AuditService adds Encryption. No new classes needed.
- Order matters: Decorators wrap from inside out. The innermost decorator runs first. In scenario 3, CallerDecorator adds class name, then LevelDecorator adds level, then TimestampDecorator adds timestamp.
- Same interface throughout: FileLogger, TimestampDecorator, LevelDecorator all implement
Logger. The client works withLoggertype regardless of how many decorators are stacked. - Open/Closed Principle: Adding a new decorator (e.g., ThreadNameDecorator) requires only one new class. No existing code changes.
8. When to Use and When NOT to Use
When to use Decorator
| Scenario | Example | Why Decorator |
|---|---|---|
| Dynamic feature addition | Adding toppings to ice cream at runtime | Customer chooses features dynamically, not at compile time |
| I/O stream wrapping | BufferedReader wrapping FileReader | Add buffering, data types, encryption to any stream |
| Logging enhancements | Timestamp, level, caller decoration | Different services need different log formats |
| UI component decoration | Adding scrollbar, border, shadow to a window | Combine visual features without subclassing |
| Middleware pipeline | Authentication, logging, compression in HTTP | Stack filters in any order |
When NOT to use Decorator
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Fixed combinations | If combinations are known and few, decorator adds complexity | Simple subclassing |
| Identity matters | Decorated object has different identity (instanceof fails) | Inheritance |
| Too many small decorators | Debugging a chain of 10 decorators is hard | Builder or configuration object |
Decorator vs Inheritance
| Aspect | Decorator | Inheritance |
|---|---|---|
| Mechanism | Composition (HAS-A) | IS-A relationship |
| When | Runtime (dynamic) | Compile time (static) |
| Combinations | Any order, any count | One fixed hierarchy |
| Class count | N components + M decorators | N x M combination classes |
| Open/Closed | Yes (add new decorator) | Partial (add subclass, but hierarchy grows) |
9. Quick Reference Card
| Component | Role | In Logging Project | In Ice Cream |
|---|---|---|---|
| Component Interface | Common type for all | Logger | Icecream |
| Concrete Component | Base object | FileLogger | VanilaIcecream |
| Abstract Decorator | Wraps + delegates | LoggerDecorator | IceCreamDecorator |
| Concrete Decorator | Adds one feature | TimestampDecorator | DryFruitDecorator |