Mediumstructural15 min read

Decorator / Wrapper Pattern

1. What is Decorator Pattern?

Definition

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

Problem

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

Solution

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.

Basic Objects (Concrete Components): [Vanilla] [Strawberry] [ButterScotch] Decorators (wrap any component): [DryFruitDecorator] [HoneyDecorator] [ChocoChipsDecorator] [CookiesDecorator] Customer 1 wants: DryFruit Vanilla IceCream --> new DryFruitDecorator(new VanilaIcecream()) Customer 2 wants: DryFruit + Honey + Cookies ButterScotch IceCream --> new DryFruitDecorator( new HoneyDecorator( new CookiesDecorator( new ButterScotchIcecream()))) No new classes needed! Just stack decorators.

Four Components

ComponentRoleIce Cream Example
a) Component InterfaceThe common interface implemented by all concrete classes and decorators to make a single typeIcecream
b) Concrete ComponentsClasses implementing Component Interface providing basic objects on which we may need to add additional functionalitiesVanilaIcecream, ButterScotchIcecream, StrawberryIcecream
c) Abstract DecoratorThe 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 DecoratorsExtends from Abstract Decorator having logics to provide additional functionalities for the Concrete Component classes objects.DryFruitDecorator, HoneyDecorator, ChocoChipsDecorator
Component Interface: Icecream | +----+----+ | | Concrete Abstract Decorator Components (IceCreamDecorator) (Vanilla, implements Icecream Strawberry, HAS-A Icecream <--- key: holds reference to wrapped component ButterScotch) | +--+--+--+--+ | | | | DryFruit Honey ChocoChips Cookies Decorator Decorator Decorator Decorator

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();
    }
}
=== Customer 1 === Preparing Vanilla Icecream + Adding Dry Fruits === Customer 2 === Preparing ButterScotch Icecream + Adding Cookies + Adding Honey + Adding Dry Fruits === Customer 3 === Preparing Strawberry Icecream + Adding Choco Chips

5. Dosa Example (UseCase 2 from course)

ComponentClasses
Component InterfaceDosa
Concrete ComponentsGreenGramDosa, BlackGramDosa, RagiDosa, MilletDosa
Abstract DecoratorBaseDosaDecorator
Concrete DecoratorsPaneerDosaDecorator, 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());
Preparing GreenGram Dosa (base) + Adding Masala filling + Adding Paneer topping Total cost: Rs 100.0

6. Pre-defined Decorator Examples in Java

JDK I/O Streams as Decorators

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)WrapsAdds
BufferedReaderReaderBuffering for efficient reading, readLine()
BufferedWriterWriterBuffering for efficient writing
DataInputStreamInputStreamreadInt(), readDouble(), readUTF()
DataOutputStreamOutputStreamwriteInt(), writeDouble(), writeUTF()
ObjectInputStreamInputStreamreadObject() for deserialization
ObjectOutputStreamOutputStreamwriteObject() for serialization
PrintStreamOutputStreamprint(), println(), printf()
InputStreamReaderInputStreamByte-to-character bridge (also Adapter)

7. Full-Length Industry Project: Logging Pipeline with Decorators

Business Scenario

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)

+---------------------------------------------------+ | SERVICE LAYER | | AccountService --> needs: timestamp + level | | TransferService --> needs: timestamp + level + caller| | AuditService --> needs: all + encryption | +----------------------------+----------------------+ | v +---------------------------------------------------+ | DECORATOR PIPELINE | | | | Logger (interface) | | | | | +--+----+ | | | | | | FileLogger LoggerDecorator (abstract) | | (base) | | | +---+---+---+---+ | | | | | | | | Timestamp Level Caller Encrypt | | Decorator Decorator Decorator Decorator | | | | Usage: new TimestampDecorator( | | new LevelDecorator( | | new FileLogger("app.log"))) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | Logger (interface) | +-------------------------------------------------------+ | + log(message: String): void | | + close(): void | +-------------------------------------------------------+ ^ ^ | | +------------------+ +---------------------------+ | FileLogger | | LoggerDecorator (abstract) | | (concrete comp) | | implements Logger | | - writer | | - wrapped: Logger [HAS-A] | +------------------+ +---------------------------+ ^ +--------------+---+---+---+ | | | | +------------+ +----------+ +----------+ +-------------+ | Timestamp | | Level | | Caller | | Encryption | | Decorator | | Decorator| | Decorator| | Decorator | +------------+ +----------+ +----------+ +-------------+ prepends prepends prepends encrypts [timestamp] [LEVEL] [ClassName] sensitive data

Project Structure

nbi-logging-pipeline/ ├── src/ │ └── main/java/com/nbi/logging/ │ ├── Logger.java (component interface) │ ├── FileLogger.java (concrete component) │ ├── LoggerDecorator.java (abstract decorator) │ ├── TimestampDecorator.java (concrete decorator) │ ├── LevelDecorator.java (concrete decorator) │ ├── CallerDecorator.java (concrete decorator) │ ├── EncryptionDecorator.java (concrete decorator) │ └── Application.java ├── src/ │ └── test/java/com/nbi/logging/ │ └── LoggerDecoratorTest.java └── pom.xml

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

=== Scenario 1: Plain Logger === Application started Processing request === Scenario 2: Timestamp + Level === [2026-11-02 18:00:00.001] [INFO] Account SAV2001 balance queried [2026-11-02 18:00:00.002] [INFO] Balance: Rs 250000 === Scenario 3: Timestamp + Level + Caller === [2026-11-02 18:00:00.003] [INFO] [TransferService] Transfer initiated: SAV2001 -> CUR3001 [2026-11-02 18:00:00.004] [INFO] [TransferService] Amount: Rs 50000 === Scenario 4: All + Encryption === [2026-11-02 18:00:00.005] [AUDIT] [AuditService] [ENCRYPTED] U2Vuc2l0aXZlOiBDdXN0b21lciBQQU4gQUJDREUxMjM0RiBhY2Nlc3NlZA== === Scenario 5: ERROR level === [2026-11-02 18:00:00.006] [ERROR] Database connection failed

Key observations from the project

  1. 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.
  2. 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.
  3. 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.
  4. Same interface throughout: FileLogger, TimestampDecorator, LevelDecorator all implement Logger. The client works with Logger type regardless of how many decorators are stacked.
  5. 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

ScenarioExampleWhy Decorator
Dynamic feature additionAdding toppings to ice cream at runtimeCustomer chooses features dynamically, not at compile time
I/O stream wrappingBufferedReader wrapping FileReaderAdd buffering, data types, encryption to any stream
Logging enhancementsTimestamp, level, caller decorationDifferent services need different log formats
UI component decorationAdding scrollbar, border, shadow to a windowCombine visual features without subclassing
Middleware pipelineAuthentication, logging, compression in HTTPStack filters in any order

When NOT to use Decorator

ScenarioWhy NOTUse Instead
Fixed combinationsIf combinations are known and few, decorator adds complexitySimple subclassing
Identity mattersDecorated object has different identity (instanceof fails)Inheritance
Too many small decoratorsDebugging a chain of 10 decorators is hardBuilder or configuration object

Decorator vs Inheritance

AspectDecoratorInheritance
MechanismComposition (HAS-A)IS-A relationship
WhenRuntime (dynamic)Compile time (static)
CombinationsAny order, any countOne fixed hierarchy
Class countN components + M decoratorsN x M combination classes
Open/ClosedYes (add new decorator)Partial (add subclass, but hierarchy grows)

9. Quick Reference Card

ComponentRoleIn Logging ProjectIn Ice Cream
Component InterfaceCommon type for allLoggerIcecream
Concrete ComponentBase objectFileLoggerVanilaIcecream
Abstract DecoratorWraps + delegatesLoggerDecoratorIceCreamDecorator
Concrete DecoratorAdds one featureTimestampDecoratorDryFruitDecorator
Structural GOF Pattern Medium

Decorator / Wrapper Design Pattern in Java

Complete Tutorial | 10 Sections | Full Logging Pipeline Project with HLD and LLD | Ice Cream and Dosa Examples | Composition over Inheritance | JDK I/O Streams | Multiple Scenarios | 12 Interview Questions

1. What is Decorator Pattern?

Definition

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

Problem

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

Solution

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.

Basic Objects (Concrete Components): [Vanilla] [Strawberry] [ButterScotch] Decorators (wrap any component): [DryFruitDecorator] [HoneyDecorator] [ChocoChipsDecorator] [CookiesDecorator] Customer 1 wants: DryFruit Vanilla IceCream --> new DryFruitDecorator(new VanilaIcecream()) Customer 2 wants: DryFruit + Honey + Cookies ButterScotch IceCream --> new DryFruitDecorator( new HoneyDecorator( new CookiesDecorator( new ButterScotchIcecream()))) No new classes needed! Just stack decorators.

Four Components

ComponentRoleIce Cream Example
a) Component InterfaceThe common interface implemented by all concrete classes and decorators to make a single typeIcecream
b) Concrete ComponentsClasses implementing Component Interface providing basic objects on which we may need to add additional functionalitiesVanilaIcecream, ButterScotchIcecream, StrawberryIcecream
c) Abstract DecoratorThe 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 DecoratorsExtends from Abstract Decorator having logics to provide additional functionalities for the Concrete Component classes objects.DryFruitDecorator, HoneyDecorator, ChocoChipsDecorator
Component Interface: Icecream | +----+----+ | | Concrete Abstract Decorator Components (IceCreamDecorator) (Vanilla, implements Icecream Strawberry, HAS-A Icecream <--- key: holds reference to wrapped component ButterScotch) | +--+--+--+--+ | | | | DryFruit Honey ChocoChips Cookies Decorator Decorator Decorator Decorator

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();
    }
}
=== Customer 1 === Preparing Vanilla Icecream + Adding Dry Fruits === Customer 2 === Preparing ButterScotch Icecream + Adding Cookies + Adding Honey + Adding Dry Fruits === Customer 3 === Preparing Strawberry Icecream + Adding Choco Chips

5. Dosa Example (UseCase 2 from course)

ComponentClasses
Component InterfaceDosa
Concrete ComponentsGreenGramDosa, BlackGramDosa, RagiDosa, MilletDosa
Abstract DecoratorBaseDosaDecorator
Concrete DecoratorsPaneerDosaDecorator, 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());
Preparing GreenGram Dosa (base) + Adding Masala filling + Adding Paneer topping Total cost: Rs 100.0

6. Pre-defined Decorator Examples in Java

JDK I/O Streams as Decorators

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)WrapsAdds
BufferedReaderReaderBuffering for efficient reading, readLine()
BufferedWriterWriterBuffering for efficient writing
DataInputStreamInputStreamreadInt(), readDouble(), readUTF()
DataOutputStreamOutputStreamwriteInt(), writeDouble(), writeUTF()
ObjectInputStreamInputStreamreadObject() for deserialization
ObjectOutputStreamOutputStreamwriteObject() for serialization
PrintStreamOutputStreamprint(), println(), printf()
InputStreamReaderInputStreamByte-to-character bridge (also Adapter)

7. Full-Length Industry Project: Logging Pipeline with Decorators

Business Scenario

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)

+---------------------------------------------------+ | SERVICE LAYER | | AccountService --> needs: timestamp + level | | TransferService --> needs: timestamp + level + caller| | AuditService --> needs: all + encryption | +----------------------------+----------------------+ | v +---------------------------------------------------+ | DECORATOR PIPELINE | | | | Logger (interface) | | | | | +--+----+ | | | | | | FileLogger LoggerDecorator (abstract) | | (base) | | | +---+---+---+---+ | | | | | | | | Timestamp Level Caller Encrypt | | Decorator Decorator Decorator Decorator | | | | Usage: new TimestampDecorator( | | new LevelDecorator( | | new FileLogger("app.log"))) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | Logger (interface) | +-------------------------------------------------------+ | + log(message: String): void | | + close(): void | +-------------------------------------------------------+ ^ ^ | | +------------------+ +---------------------------+ | FileLogger | | LoggerDecorator (abstract) | | (concrete comp) | | implements Logger | | - writer | | - wrapped: Logger [HAS-A] | +------------------+ +---------------------------+ ^ +--------------+---+---+---+ | | | | +------------+ +----------+ +----------+ +-------------+ | Timestamp | | Level | | Caller | | Encryption | | Decorator | | Decorator| | Decorator| | Decorator | +------------+ +----------+ +----------+ +-------------+ prepends prepends prepends encrypts [timestamp] [LEVEL] [ClassName] sensitive data

Project Structure

nbi-logging-pipeline/ ├── src/ │ └── main/java/com/nbi/logging/ │ ├── Logger.java (component interface) │ ├── FileLogger.java (concrete component) │ ├── LoggerDecorator.java (abstract decorator) │ ├── TimestampDecorator.java (concrete decorator) │ ├── LevelDecorator.java (concrete decorator) │ ├── CallerDecorator.java (concrete decorator) │ ├── EncryptionDecorator.java (concrete decorator) │ └── Application.java ├── src/ │ └── test/java/com/nbi/logging/ │ └── LoggerDecoratorTest.java └── pom.xml

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

=== Scenario 1: Plain Logger === Application started Processing request === Scenario 2: Timestamp + Level === [2026-11-02 18:00:00.001] [INFO] Account SAV2001 balance queried [2026-11-02 18:00:00.002] [INFO] Balance: Rs 250000 === Scenario 3: Timestamp + Level + Caller === [2026-11-02 18:00:00.003] [INFO] [TransferService] Transfer initiated: SAV2001 -> CUR3001 [2026-11-02 18:00:00.004] [INFO] [TransferService] Amount: Rs 50000 === Scenario 4: All + Encryption === [2026-11-02 18:00:00.005] [AUDIT] [AuditService] [ENCRYPTED] U2Vuc2l0aXZlOiBDdXN0b21lciBQQU4gQUJDREUxMjM0RiBhY2Nlc3NlZA== === Scenario 5: ERROR level === [2026-11-02 18:00:00.006] [ERROR] Database connection failed

Key observations from the project

  1. 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.
  2. 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.
  3. 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.
  4. Same interface throughout: FileLogger, TimestampDecorator, LevelDecorator all implement Logger. The client works with Logger type regardless of how many decorators are stacked.
  5. 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

ScenarioExampleWhy Decorator
Dynamic feature additionAdding toppings to ice cream at runtimeCustomer chooses features dynamically, not at compile time
I/O stream wrappingBufferedReader wrapping FileReaderAdd buffering, data types, encryption to any stream
Logging enhancementsTimestamp, level, caller decorationDifferent services need different log formats
UI component decorationAdding scrollbar, border, shadow to a windowCombine visual features without subclassing
Middleware pipelineAuthentication, logging, compression in HTTPStack filters in any order

When NOT to use Decorator

ScenarioWhy NOTUse Instead
Fixed combinationsIf combinations are known and few, decorator adds complexitySimple subclassing
Identity mattersDecorated object has different identity (instanceof fails)Inheritance
Too many small decoratorsDebugging a chain of 10 decorators is hardBuilder or configuration object

Decorator vs Inheritance

AspectDecoratorInheritance
MechanismComposition (HAS-A)IS-A relationship
WhenRuntime (dynamic)Compile time (static)
CombinationsAny order, any countOne fixed hierarchy
Class countN components + M decoratorsN x M combination classes
Open/ClosedYes (add new decorator)Partial (add subclass, but hierarchy grows)

9. Quick Reference Card

ComponentRoleIn Logging ProjectIn Ice Cream
Component InterfaceCommon type for allLoggerIcecream
Concrete ComponentBase objectFileLoggerVanilaIcecream
Abstract DecoratorWraps + delegatesLoggerDecoratorIceCreamDecorator
Concrete DecoratorAdds one featureTimestampDecoratorDryFruitDecorator