SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignDecorator Pattern
Software DesignStructural Patterns

Decorator Pattern

Add behavior to objects dynamically by wrapping them — a flexible alternative to subclassing.

October 19, 20245 min read
design-patternsjavadecoratorstructural

One-line Definition

Encapsulates an object by wrapping it inside special wrapper objects, allowing you to attach new behaviors dynamically.


Problem

Suppose you have a notification system. Initially, it sends emails. Then you add SMS. Then Slack. If you try to support all combinations via subclassing, you'll end up with EmailNotifier, SMSNotifier, SlackNotifier, EmailSMSNotifier, EmailSlackNotifier, SMSSlackNotifier, and EmailSMSSlackNotifier. That's an exponential class explosion. The Decorator pattern solves this by letting you stack independent behaviors on top of a base object dynamically at runtime.


Real-world Analogy

Think of a plain coffee. You can add milk, add sugar, add whipped cream — each topping wraps the original coffee without changing it. You pay the base price plus each add-on. The barista doesn't need a separate recipe for "MilkSugarWhippedCreamCoffee" — each decorator adds its own cost and description on top.


Structure

  • Client
    • Wraps components in multiple layers of decorators dynamically.
  • Component
    • Interface defining the operations that can be decorated.
  • ConcreteComponent
    • The base object being decorated (e.g., SimpleCoffee).
  • Decorator
    • Abstract class that wraps a Component and delegates calls to it.
  • ConcreteDecorator
    • Adds specific behavior before or after delegating to the wrapped component.

Diagram

classDiagram
    class Client {
    }
    class Component {
        <<interface>>
        +operation()
    }
    class ConcreteComponent {
        +operation()
    }
    class Decorator {
        <<abstract>>
        -Component wrappee
        +operation()
    }
    class ConcreteDecoratorA {
        +operation()
    }
    class ConcreteDecoratorB {
        +operation()
    }
    
    Client --> Component
    ConcreteComponent ..|> Component
    Decorator ..|> Component
    Decorator o-- Component : wraps
    ConcreteDecoratorA --|> Decorator
    ConcreteDecoratorB --|> Decorator

Code Walkthrough

Notice how the Client dynamically stacks multiple decorators onto the SimpleCoffee. The base object remains completely untouched while gaining new behaviors.

interface Coffee {
    double getCost();
    String getDescription();
}

class SimpleCoffee implements Coffee {
    @Override
    public double getCost() { return 50.0; }
    @Override
    public String getDescription() { return "Simple Coffee"; }
}

abstract class CoffeeDecorator implements Coffee {
    protected final Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public double getCost() { return decoratedCoffee.getCost(); }
    @Override
    public String getDescription() { return decoratedCoffee.getDescription(); }
}

class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }

    @Override
    public double getCost() { return super.getCost() + 15.0; }
    @Override
    public String getDescription() { return super.getDescription() + ", Milk"; }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) { super(coffee); }

    @Override
    public double getCost() { return super.getCost() + 5.0; }
    @Override
    public String getDescription() { return super.getDescription() + ", Sugar"; }
}

class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.getDescription() + " → ₹" + coffee.getCost());

        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.getDescription() + " → ₹" + coffee.getCost());

        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.getDescription() + " → ₹" + coffee.getCost());
    }
}

Bad vs Good

Bad Approach

Problems

  • Creating a subclass for every combination causes an exponential class explosion.
  • You can't dynamically add toppings at runtime; you must instantiate the exact subclass.
  • Modifying pricing logic requires changing dozens of hardcoded classes.
class MilkCoffee extends SimpleCoffee {
    @Override
    public double getCost() { return 65.0; }
    @Override
    public String getDescription() { return "Coffee with Milk"; }
}

class MilkSugarCoffee extends SimpleCoffee {
    @Override
    public double getCost() { return 70.0; }
    @Override
    public String getDescription() { return "Coffee with Milk and Sugar"; }
}

// ... exponential growth as more toppings are added ...

Better Approach

Improvements

  • Each topping is represented by exactly one class.
  • Behaviors are stacked dynamically at runtime.
  • New combinations require absolutely no new classes.
Coffee order = new SimpleCoffee();
order = new MilkDecorator(order);
order = new SugarDecorator(order);

// Combination decided at runtime, no subclass explosion
System.out.println(order.getDescription() + " → ₹" + order.getCost());

Pros vs Cons

ProsCons
Avoids exponential subclass explosionMany small wrapper classes can clutter the codebase
Add or remove responsibilities dynamically at runtimeDebugging deeply wrapped objects can be highly confusing
Combines multiple behaviors by wrapping in multiple layersOrder of wrapping can matter and introduce subtle bugs
Follows the Single Responsibility PrincipleClient code can become ugly (e.g., new A(new B(new C())))
Protects base classes from modification (Open/Closed Principle)

When to Use

  • You need to assign extra behaviors to objects at runtime without breaking the code that uses these objects.
  • It's awkward or impossible to extend an object's behavior using inheritance.
  • You have orthogonal behaviors that can be mixed and matched in numerous combinations.

When Not to Use

  • The order of decorators strongly dictates the final behavior, leading to brittle initialization.
  • The combinations are entirely fixed and finite; a small, flat subclass hierarchy might be more readable.

Real-world Examples

  • java.io.InputStream, OutputStream, Reader, and Writer (e.g., new BufferedReader(new InputStreamReader(new FileInputStream(...))))
  • java.util.Collections#synchronizedList(List)
  • javax.servlet.http.HttpServletRequestWrapper

Key Takeaway

The Decorator Pattern provides a highly flexible alternative to subclassing by letting you wrap an object with multiple independent layers of behavior at runtime. Use it to prevent an explosion of combinatorial subclasses, but be wary of creating code that looks like an infinitely nested Russian doll.

PreviousFactory PatternNextTemplate Method Pattern