Skip to main content

Command Palette

Search for a command to run...

Decorator Design Pattern

Updated
4 min read
Decorator Design Pattern

Software systems rarely stay the same for long. Features evolve, requirements change, and behavior that was once sufficient often needs to be extended.

One common challenge is adding new functionality to an existing object without modifying its code. At first glance, this may seem simple, but it can quickly lead to rigid designs and class explosion if not handled carefully.

The Decorator Design Pattern addresses this problem by allowing behavior to be added dynamically to individual objects while keeping the original class unchanged.

The key idea is composition instead of inheritance.

Rather than creating multiple subclasses for every possible combination of features, we wrap an object inside another object that implements the same interface. Each wrapper (decorator) adds its own behavior and delegates the remaining work to the wrapped object.

The Problem

Imagine a coffee shop application.

Initially, we only have a simple coffee.

Coffee coffee = new SimpleCoffee();

Later, customers want additional options:

  • Milk

  • Sugar

  • Whipped Cream

A common approach is inheritance:

SimpleCoffee
├─ CoffeeWithMilk
├─ CoffeeWithSugar
├─ CoffeeWithWhip
├─ CoffeeWithMilkAndSugar
├─ CoffeeWithMilkAndWhip
└─ CoffeeWithMilkSugarAndWhip

As more toppings are added, the number of classes grows quickly. Maintaining these combinations becomes difficult and inflexible.

The real issue is that toppings are optional behaviors that should be added dynamically, not fixed through inheritance.

The Solution

Decorator solves this by wrapping an object inside another object.

The structure looks like this:

Component (Coffee)
        ↑
SimpleCoffee

Decorator
        ↑
MilkDecorator
SugarDecorator
WhipDecorator

Every decorator implements the same interface as the original object and delegates work to the wrapped object while adding its own behavior.

Coffee Example

First, define the common interface.

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

Base coffee implementation:

public class SimpleCoffee implements Coffee {

    @Override
    public double getCost() {
        return 100;
    }

    @Override
    public String getDescription() {
        return "Simple Coffee";
    }
}

Abstract decorator:

public abstract class CoffeeDecorator implements Coffee {

    protected Coffee coffee;

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

    @Override
    public double getCost() {
        return coffee.getCost();
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }
}

Milk decorator:

public class MilkDecorator extends CoffeeDecorator {

    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return super.getCost() + 50;
    }

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

Sugar decorator:

public class SugarDecorator extends CoffeeDecorator {
    @override
    double getCost() {
        return coffee.getCost() + 20.00;
    }
    
    @override
    String getDescription() {
        return coffee.getDescription + ", Sugar";
    }  
}

Whip decorator:

public class WhipDecorator extends CoffeeDecorator {
    @override
    double getCost() {
        return coffee.getCost() + 70.00;
    }
    
    @override
    String getDescription() {
        return coffee.getDescription + ", Whip";
    }  
}

Building a Coffee

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        print(coffee);
        coffee = new MilkDecorator(coffee);
        print(coffee);
        coffee = new SugarDecorator(coffee);
        print(coffee);
        coffee = new WhipDecorator(coffee);
        print(coffee);

        Coffee special = new WhipDecorator(
                         new SugarDecorator(
                         new MilkDecorator{
                         new SimpleCoffee())));
        print(special);
    }
}
Coffee coffee =
    new WhipDecorator(
        new SugarDecorator(
            new MilkDecorator(
                new SimpleCoffee()
            )
        )
    );

Visually:

WhipDecorator
      ↓
SugarDecorator
      ↓
MilkDecorator
      ↓
SimpleCoffee

When getCost() is called, each layer contributes its own cost.

Simple Coffee = 100
Milk          = +50
Sugar         = +20
Whip          = +70
-------------------
Total         = 240

The client only interacts with the outermost object and remains unaware of the internal chain.

Why Decorator Works Well

Runtime Flexibility

Features can be added dynamically.

coffee = new MilkDecorator(coffee);

Avoids Class Explosion

We don't need separate classes for every topping combination.

Follows Open-Closed Principle

Existing classes remain unchanged while new functionality can be added through new decorators.

Encourages Composition

Behavior is assembled from small reusable components rather than inherited from large hierarchies.

Tradeoffs

Decorator is not free.

A heavily decorated object can become deeply nested:

new WhipDecorator(
    new SugarDecorator(
        new MilkDecorator(
            new SimpleCoffee()
        )
    )
);

This can make debugging slightly harder because method calls pass through multiple layers.

Also, the order of decorators can matter in some scenarios, especially when decorators modify behavior rather than simply add values.

Summary

The Decorator Pattern is a clean way to extend functionality without modifying existing classes. It replaces rigid inheritance hierarchies with flexible composition, making systems easier to extend as requirements evolve.

Whenever you see many subclasses being created just to support different combinations of features, it is often a sign that a decorator-based design may be a better solution.