Decorator Design Pattern in Java: Adding New Behavior Without Modifying Existing Code

Description / Meta Description

Learn the Decorator Design Pattern in Java with practical examples. Understand how to add functionality dynamically without modifying existing classes. Discover real-world examples from Java I/O, Spring Framework, and enterprise applications.


Decorator Design Pattern in Java: Adding New Behavior Without Modifying Existing Code

In the previous article, we explored the Adapter Pattern, which helps incompatible systems work together.

Adapter solves:

“How do I make two incompatible interfaces communicate?”

Decorator solves a completely different problem:

“How do I add new functionality to an object without modifying its existing code?”

This is one of the most elegant and widely used Structural Design Patterns.

In fact, you’ve probably used Decorator hundreds of times without realizing it.


The Problem: Feature Explosion

Suppose you’re building a notification system.

Basic notification:

public interface Notification {

    void send();
}

Implementation:

public class EmailNotification
        implements Notification {

    @Override
    public void send() {

        System.out.println(
            "Sending Email");
    }
}

Business is happy.

For one week.

Then requirements arrive:

  • Add Logging
  • Add Encryption
  • Add Audit Trail
  • Add Retry Logic
  • Add Compression

Many developers immediately think:

EmailNotification
    ↓
LoggedEmailNotification
    ↓
EncryptedLoggedEmailNotification
    ↓
EncryptedLoggedRetriableEmailNotification

Class explosion begins.


Why Inheritance Becomes a Problem

Imagine:

Email Notification
SMS Notification
Push Notification

Now each needs:

Logging
Retry
Audit
Compression
Encryption

Potential combinations:

Email + Logging
Email + Retry
Email + Logging + Retry

SMS + Logging
SMS + Retry
SMS + Logging + Retry

Push + Logging
Push + Retry
Push + Logging + Retry

Number of classes grows exponentially.

Maintenance becomes a nightmare.


What We Really Want

Instead of creating:

LoggedEncryptedRetriableEmailNotification

we want:

EmailNotification
       │
       ▼
LoggingDecorator
       │
       ▼
EncryptionDecorator
       │
       ▼
RetryDecorator

Each behavior remains independent.

This is exactly what Decorator provides.


What is Decorator Pattern?

Decorator is a Structural Design Pattern that:

Adds new behavior to an object dynamically without changing its existing code.

Think of it like:

Coffee
   │
Milk
   │
Sugar
   │
Whipped Cream

You start with a basic coffee.

You decorate it with additional features.

The original coffee remains unchanged.


Architecture Diagram

Client
   │
   ▼

Notification
   ▲
   │

EmailNotification

   ▲
   │

LoggingDecorator

   ▲
   │

EncryptionDecorator

Each decorator wraps another object.


Step 1: Define Common Interface

public interface Notification {

    void send();
}

Step 2: Concrete Component

public class EmailNotification
        implements Notification {

    @Override
    public void send() {

        System.out.println(
            "Sending Email");
    }
}

Step 3: Base Decorator

public abstract class NotificationDecorator
        implements Notification {

    protected Notification notification;

    public NotificationDecorator(
            Notification notification) {

        this.notification = notification;
    }

    @Override
    public void send() {

        notification.send();
    }
}

Notice:

Decorator HAS-A Notification

This is composition.

Not inheritance.


Step 4: Logging Decorator

public class LoggingDecorator
        extends NotificationDecorator {

    public LoggingDecorator(
            Notification notification) {

        super(notification);
    }

    @Override
    public void send() {

        System.out.println(
            "Logging Request");

        super.send();
    }
}

Step 5: Encryption Decorator

public class EncryptionDecorator
        extends NotificationDecorator {

    public EncryptionDecorator(
            Notification notification) {

        super(notification);
    }

    @Override
    public void send() {

        System.out.println(
            "Encrypting Message");

        super.send();
    }
}

Step 6: Client Usage

Notification notification =
        new EncryptionDecorator(
            new LoggingDecorator(
                new EmailNotification()));

notification.send();

Output:

Encrypting Message
Logging Request
Sending Email

Notice:

No existing code changed.

New behavior added dynamically.


How Decorator Works Internally

Execution flow:

Client
   │
   ▼

EncryptionDecorator
   │
   ▼

LoggingDecorator
   │
   ▼

EmailNotification

Each layer adds functionality.

Then delegates to the wrapped object.


Real-World Example: Coffee Shop

Classic Decorator example.

Base coffee:

Coffee coffee =
        new BasicCoffee();

Add milk:

coffee =
        new MilkDecorator(coffee);

Add sugar:

coffee =
        new SugarDecorator(coffee);

Add whipped cream:

coffee =
        new WhippedCreamDecorator(coffee);

Result:

Basic Coffee
+ Milk
+ Sugar
+ Whipped Cream

The original coffee class never changes.


Java I/O: The Most Famous Decorator Example

Most Java developers have already used Decorator.

Example:

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

Now decorate:

InputStream buffered =
        new BufferedInputStream(input);

Further decorate:

DataInputStream dataInput =
        new DataInputStream(buffered);

Architecture:

FileInputStream
       │
BufferedInputStream
       │
DataInputStream

Each layer adds functionality.

This is textbook Decorator Pattern.


Spring Framework Examples

Decorator appears throughout Spring.


HttpServletRequest Wrappers

HttpServletRequest

Decorated with:

Security
Caching
Logging
Auditing

without changing original request objects.


Spring Security Filters

Chain processing often behaves similarly:

Authentication
      ↓
Authorization
      ↓
Audit
      ↓
Request Processing

Each layer adds behavior.


Decorator vs Inheritance

This is the key design principle.


Inheritance Approach

EmailNotification
      │
      ▼

LoggedEmailNotification
      │
      ▼

EncryptedLoggedEmailNotification

Problems:

  • rigid hierarchy
  • class explosion
  • difficult maintenance

Decorator Approach

EmailNotification
       │
       ▼

LoggingDecorator
       │
       ▼

EncryptionDecorator

Benefits:

  • reusable
  • flexible
  • dynamic

Decorator vs Adapter

A common interview question.


Adapter

Purpose:

Change Interface

Example:

pay()
   ↓
makePayment()

Decorator

Purpose:

Keep Interface
Add Behavior

Example:

send()
   ↓
Logged send()
   ↓
Encrypted send()

Decorator vs Proxy

Another common confusion.


Decorator

Goal:

Enhance Functionality

Proxy

Goal:

Control Access

Examples:

Security Checks
Lazy Loading
Remote Calls
Caching

Proxy protects.

Decorator enhances.


Benefits of Decorator Pattern

1. Open/Closed Principle

Add behavior.

Avoid modifying existing classes.


2. Flexible Composition

Mix and match features.


3. Avoid Class Explosion

No endless inheritance trees.


4. Reusable Components

Decorators work across multiple objects.


5. Runtime Configuration

Behavior can be assembled dynamically.


Common Mistakes


Mistake 1: Too Many Decorator Layers

Example:

Decorator
Decorator
Decorator
Decorator
Decorator

Debugging becomes harder.


Mistake 2: Business Logic Leakage

Decorators should enhance behavior.

Not contain core business workflows.


Mistake 3: Confusing Decorator with Inheritance

If behavior is static and permanent, inheritance may be simpler.

Use Decorator when flexibility matters.


When Should You Use Decorator?

Use Decorator when:

✔ Functionality should be added dynamically

✔ Modifying existing code is undesirable

✔ Multiple feature combinations exist

✔ Inheritance becomes messy

✔ Open/Closed Principle is important


Avoid Decorator when:

❌ Behavior never changes

❌ Simpler inheritance works

❌ Extra abstraction adds little value


Quick Comparison

PatternPurpose
AdapterMake interfaces compatible
DecoratorAdd behavior dynamically
ProxyControl access
FacadeSimplify complexity

Final Thoughts

The Decorator Pattern is one of the most elegant examples of:

“Favor Composition Over Inheritance.”

Instead of creating endless subclasses, Decorator allows functionality to be assembled like building blocks.

This is why Decorator appears heavily in:

  • Java I/O
  • Spring Framework
  • Logging frameworks
  • Security layers
  • Enterprise middleware

Whenever you need to extend behavior without modifying existing code, Decorator should be one of the first patterns you consider.

In the next article, we’ll continue our Structural Design Pattern journey with:

Facade Pattern — Hiding Complex Systems Behind a Simple Interface

You’ll learn why frameworks like Spring’s JdbcTemplate, RestTemplate, and many enterprise APIs are classic examples of the Facade pattern.

Leave a Reply

Your email address will not be published. Required fields are marked *