Java Decorator Pattern: Add Dynamic Behavior with Wrappers and Practical Examples

0

Decorator Pattern

Imagine ordering a basic coffee and then adding milk, sugar, whipped cream, caramel, or an extra espresso shot. You do not create an entirely different coffee class for every possible combination. Instead, you start with the basic coffee and wrap it with additional features.

The Decorator Pattern applies the same idea to software. It allows behavior or responsibilities to be added to an individual object dynamically without modifying its original class.

Decorator is a structural design pattern. It uses composition and wrapping to extend an object's behavior while keeping the original interface intact.

Why Does the Decorator Pattern Exist?

A common object-oriented programming problem appears when a class needs many optional features. One tempting solution is to create subclasses for every possible combination.

Suppose a coffee application supports:

  • Basic coffee
  • Milk
  • Sugar
  • Whipped cream
  • Caramel

Creating separate classes for every combination quickly becomes unpleasant:

CoffeeWithMilk
CoffeeWithSugar
CoffeeWithMilkAndSugar
CoffeeWithMilkSugarAndCream
CoffeeWithMilkSugarCreamAndCaramel
...

The number of combinations can grow rapidly as features increase.

Decorator solves this by allowing features to be composed at runtime.

Important: Decorator adds responsibilities to an object without requiring changes to the original class or creating a subclass for every feature combination.

Decorator Pattern in Simple Words

Think of wrapping a gift. You start with the gift itself, then add wrapping paper, a ribbon, and a greeting card. Each layer surrounds the previous one.

The final object still represents the same underlying gift, but it now has additional characteristics.

Software decoration works similarly:

Basic Object
     |
     v
Decorator A
     |
     v
Decorator B
     |
     v
Decorator C
     |
     v
Final Object

Each decorator can add behavior before or after delegating to the wrapped object.

Core Structure of Decorator

A typical Decorator implementation contains four important participants:

  • Component: Defines the common interface.
  • Concrete Component: Provides the original behavior.
  • Decorator: Implements the same interface and holds a Component.
  • Concrete Decorator: Adds a specific responsibility.

A Simple Java Example

Let's begin with a simple coffee interface.

interface Coffee {

    String getDescription();

    double getCost();
}

The basic coffee is the concrete component.

class BasicCoffee implements Coffee {

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

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

Now create an abstract decorator that also implements Coffee.

abstract class CoffeeDecorator implements Coffee {

    protected final Coffee coffee;

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

A milk decorator can now add its own behavior.

class MilkDecorator extends CoffeeDecorator {

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

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

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

A sugar decorator can be added independently.

class SugarDecorator extends CoffeeDecorator {

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

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }

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

Now the client can combine decorators dynamically.

Coffee coffee = new BasicCoffee();

coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

System.out.println(coffee.getDescription());
System.out.println(coffee.getCost());

The resulting description is effectively:

Basic Coffee, Milk, Sugar

And the cost becomes:

130.0

The original BasicCoffee class was never modified.

How the Decorator Works Internally

The most important mechanism is that every decorator holds a reference to the same component abstraction.

SugarDecorator
       |
       v
MilkDecorator
       |
       v
BasicCoffee

When the client calls getCost(), the outer decorator can perform its own calculation and delegate to the wrapped object.

For the previous example:

SugarDecorator
    + 10
      |
      v
MilkDecorator
    + 20
      |
      v
BasicCoffee
    100

The final result is 130.

Remember: A decorator and the object it wraps implement the same component interface. That is what allows decorators to be stacked.

Adding Multiple Decorators

One of the strongest features of the Decorator Pattern is that decorators can be composed in different combinations.

Coffee coffee1 =
    new MilkDecorator(
        new BasicCoffee()
    );

Coffee coffee2 =
    new SugarDecorator(
        new MilkDecorator(
            new BasicCoffee()
        )
    );

Coffee coffee3 =
    new SugarDecorator(
        new MilkDecorator(
            new BasicCoffee()
        )
    );

The client can choose the combination at runtime instead of relying on a large inheritance hierarchy.

Order of Decorators Matters

Decorators are not always interchangeable. Their order can affect the final behavior.

Consider decorators that apply discounts:

Price
  |
  v
Discount A
  |
  v
Discount B

If Discount A is calculated before Discount B, the result may differ from applying Discount B before Discount A.

Therefore, decorators should not be treated as an unordered collection. Their sequence may be part of the business behavior.

Decorator Can Add Behavior Before Delegation

A decorator does not have to modify the result after delegation. It can execute logic before calling the wrapped component.

class LoggingDecorator implements Coffee {

    private final Coffee coffee;

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

    @Override
    public String getDescription() {
        System.out.println("Getting description...");
        return coffee.getDescription();
    }

    @Override
    public double getCost() {
        System.out.println("Calculating cost...");
        return coffee.getCost();
    }
}

This technique is useful for cross-cutting responsibilities such as logging, metrics, authorization, caching, validation, and tracing.

Decorator Can Add Behavior After Delegation

A decorator can also delegate first and then modify the result.

class TaxDecorator implements Coffee {

    private final Coffee coffee;

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

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Tax";
    }

    @Override
    public double getCost() {
        double cost = coffee.getCost();
        return cost * 1.18;
    }
}

Here, the decorator calculates tax using the price produced by the wrapped object.

Decorator for Logging

The pattern becomes especially valuable when a behavior should be attached to selected objects rather than every instance of a class.

interface OrderService {

    void placeOrder(String orderId);
}

The real service performs the business operation.

class OrderServiceImpl implements OrderService {

    @Override
    public void placeOrder(String orderId) {
        System.out.println(
            "Order placed: " + orderId
        );
    }
}

A logging decorator can wrap it.

class LoggingOrderService
        implements OrderService {

    private final OrderService service;

    public LoggingOrderService(OrderService service) {
        this.service = service;
    }

    @Override
    public void placeOrder(String orderId) {
        System.out.println(
            "Starting order: " + orderId
        );

        service.placeOrder(orderId);

        System.out.println(
            "Finished order: " + orderId
        );
    }
}

The client can now choose whether an instance should have logging.

OrderService service =
    new LoggingOrderService(
        new OrderServiceImpl()
    );

service.placeOrder("ORD-101");

Decorator for Validation

Another decorator can validate an operation before passing it to the underlying service.

class ValidationOrderService
        implements OrderService {

    private final OrderService service;

    public ValidationOrderService(OrderService service) {
        this.service = service;
    }

    @Override
    public void placeOrder(String orderId) {

        if (orderId == null || orderId.isBlank()) {
            throw new IllegalArgumentException(
                "Order ID is required"
            );
        }

        service.placeOrder(orderId);
    }
}

Now multiple responsibilities can be composed:

OrderService service =
    new LoggingOrderService(
        new ValidationOrderService(
            new OrderServiceImpl()
        )
    );

This creates a chain where validation and logging are added without changing the core service.

Decorator Chains

When several decorators are stacked, the result can be thought of as a processing pipeline.

Client
  |
  v
Logging Decorator
  |
  v
Validation Decorator
  |
  v
Caching Decorator
  |
  v
Core Service

Each layer has one responsibility and can delegate to the next layer.

Architecture Insight: Decorator chains are useful when optional responsibilities need to be composed independently. They can provide a lightweight alternative to creating large inheritance hierarchies.

Decorator vs Inheritance

Both inheritance and decoration can extend behavior, but they do so differently.

Aspect Inheritance Decorator
Relationship Class hierarchy Object composition
Behavior selection Usually fixed by type Can be selected dynamically
Combinations Can cause subclass explosion Can be composed through wrapping
Runtime flexibility Lower Higher
Original class modification Not required Not required

Decorator vs Adapter

Adapter and Decorator both use wrapping, but their intent is different.

Aspect Adapter Decorator
Primary purpose Make incompatible interfaces compatible Add responsibilities or behavior
Interface Usually changes the interface presented to the client Preserves the component interface
Main concern Compatibility Extension
Typical reason Existing API does not match client expectations Object needs optional additional behavior

Decorator vs Proxy

Decorator and Proxy can look almost identical structurally because both can wrap another object and implement the same interface. The difference is mainly their intent.

Decorator focuses on adding responsibilities. Proxy focuses on controlling access to an object.

Decorator:
Client → Decorator → Real Object
          |
          + Adds behavior

Proxy:
Client → Proxy → Real Object
          |
          + Controls access

Decorator and Open/Closed Principle

Decorator is closely related to the Open/Closed Principle. The principle encourages software entities to be open for extension while being closed for modification.

Instead of repeatedly editing an existing service whenever a new optional behavior is needed, a decorator can introduce that behavior externally.

For example, instead of modifying the original service to add logging, metrics, caching, and validation, separate decorators can provide those responsibilities.

Decorator and Single Responsibility Principle

Decorator can also support the Single Responsibility Principle. Each decorator can focus on one additional responsibility.

Core Service
     |
     +-- Logging
     |
     +-- Validation
     |
     +-- Metrics
     |
     +-- Caching

Instead of creating one large class responsible for everything, responsibilities can be separated into independently composable decorators.

Real-World Example: Java I/O

Java I/O provides one of the most recognizable examples of decorator-style design.

A basic input stream can be wrapped with another stream that adds buffering or other functionality.

InputStream input =
    new BufferedInputStream(
        new FileInputStream("data.txt")
    );

The underlying file stream provides the basic operation, while the buffered stream adds buffering behavior without changing the fundamental stream abstraction.

This is exactly the kind of compositional thinking that makes the Decorator Pattern powerful.

Dynamic Composition

One of the biggest advantages of Decorator is that the object can be composed differently depending on runtime requirements.

OrderService service =
    new OrderServiceImpl();

if (loggingEnabled) {
    service = new LoggingOrderService(service);
}

if (validationEnabled) {
    service = new ValidationOrderService(service);
}

The same core service can therefore participate in different configurations without creating additional subclasses.

Common Beginner Mistakes

1. Changing the Interface

A classic decorator normally preserves the component interface. If the main purpose is to translate one interface into another, Adapter is usually the more appropriate pattern.

2. Putting Too Much Logic in One Decorator

A decorator should ideally add one coherent responsibility. A single decorator containing logging, caching, validation, authorization, and business rules becomes difficult to understand and maintain.

3. Ignoring Decorator Order

The order of decorators can affect behavior. For example, applying a cache before authorization may have different semantics from applying authorization before the cache.

4. Creating Deeply Nested Decorators Without a Reason

Composition is powerful, but excessive nesting can make debugging and understanding execution flow difficult.

5. Using Decorator When Simple Composition Is Enough

Not every optional operation needs a formal decorator hierarchy. The pattern is most valuable when behaviors share a common interface and need to be independently composable.

Best Practices for Decorator Pattern

  • Keep the component interface small and focused.
  • Make decorators implement the same interface as the wrapped component.
  • Keep each decorator focused on one responsibility.
  • Prefer composition instead of creating large inheritance hierarchies.
  • Document ordering requirements when decorator order affects behavior.
  • Keep the core component independent from optional decorators.
  • Test decorators individually and test important decorator combinations.

Testing Decorators

A decorator should be tested for both its own behavior and its interaction with the wrapped component.

For example, a logging decorator should be tested to ensure that it delegates correctly. A validation decorator should verify invalid input without unnecessarily invoking the underlying service.

When multiple decorators are combined, integration tests can verify that the complete chain behaves in the expected order.

When Should You Use Decorator?

  • When objects need optional behavior that can be combined dynamically.
  • When subclass combinations are becoming difficult to manage.
  • When responsibilities should be added without modifying the original class.
  • When several independent cross-cutting behaviors can be composed.
  • When clients should continue using the same component interface.

When Decorator May Be Unnecessary

  • When there is only one fixed implementation and no meaningful variation.
  • When a simple method or helper is sufficient.
  • When the resulting decorator chain would be harder to understand than the original implementation.
  • When the added behavior is inseparable from the core business logic.

Interview Insight: What Problem Does Decorator Solve?

Decorator solves the problem of adding responsibilities to individual objects dynamically without modifying the original class or creating a subclass for every possible combination of behavior.

Interview Insight: Why Does Decorator Use the Same Interface?

The same interface allows a decorator to be treated exactly like the object it wraps. This makes decorators stackable and lets clients remain unaware of how many layers exist underneath.

Interview Insight: Is Decorator Inheritance or Composition?

The core mechanism is composition. A decorator contains another component and delegates operations to it, while adding its own behavior.

Interview Insight: Can Decorators Be Nested?

Yes. Nesting is one of the defining strengths of the pattern. Multiple decorators can wrap one another to form a behavior chain.

Interview Insight: Why Not Create Subclasses?

Subclassing can lead to a large number of classes when several independent optional features can be combined. Decorators allow those features to be assembled dynamically instead.

Learning Checkpoint

Before moving to the next pattern, make sure you can answer these questions:

  • What problem does the Decorator Pattern solve?
  • Why does a decorator implement the same interface as the component?
  • How does composition help avoid subclass explosion?
  • Why can decorator order matter?
  • How is Decorator different from Adapter?
  • How is Decorator different from Proxy?
  • How can decorators support the Open/Closed Principle?

Decorator Pattern Quick Revision

Concept Key Point
Pattern type Structural
Core purpose Add responsibilities to objects dynamically
Component Common interface for the object and decorators
Concrete component Original object providing core behavior
Decorator Wraps a component and delegates to it
Concrete decorator Adds a specific responsibility
Main technique Composition and delegation
Major benefit Flexible behavior composition without subclass explosion
Main caution Too many layers can make execution flow difficult to understand

Final Takeaway

The Decorator Pattern lets you extend an object's behavior by wrapping it with additional components that implement the same interface. Each decorator can add one focused responsibility while delegating the rest of the work to the object underneath.

Its real strength appears when behavior is optional, independently composable, and potentially needed in different combinations. Instead of creating a subclass for every combination, decorators let the application build the required behavior at runtime.

The key idea to remember is simple: wrap an object to add behavior while keeping the same interface.

Post a Comment

0Comments
Post a Comment (0)