DEV Community

Cover image for Mastering the Decorator Pattern: Adding Behaviour Without Breaking Code
Bolaji Ajani
Bolaji Ajani

Posted on

Mastering the Decorator Pattern: Adding Behaviour Without Breaking Code

"Open for extension, closed for modification."
This single line from the SOLID principles perfectly captures the spirit of the Decorator Pattern

When you need to extend functionality without touching existing code, the Decorator Pattern is one of the most elegant solutions in software design.

What is Decorator Pattern?

The Decorator Pattern is a structural pattern that lets you dynamically add new behavior to objects without changing their original structure.

Instead of building large inheritance trees, you “wrap” an existing object with one or more decorator classes that each add something new.

Real-World Analogy

Think of a cup of coffee:

  • You start with a base drink — say, espresso.
  • You can then add milk, sugar, or cream.

Each addition enhances the coffee’s flavor without changing the core espresso class. That’s the Decorator Pattern in action.

Does the Decorator Pattern Satisfy SOLID Principles?

S - Single Responsibility: Each decorator adds one specific concern (e.g., logging, caching) to the base object without altering its core behaviour. For example, in a coffee shop system, a MilkDecorator adds milk-related behaviour without changing the base Coffee class's responsibility.
O - Open/Closed: Extend behaviour by wrapping and not rewriting the class. This is where the Decorator Pattern excels. It allows new functionality to be added (extension) by wrapping objects with decorators, without modifying the original class's code. For instance, you can add a SugarDecorator to a Coffee object without touching the Coffee class itself.
L - Liskov Substitution: Decorators can replace the base class since they share the same interface. Decorators typically implement the same base class as the object they decorate, ensuring they can be used interchangeably. For example, a DecoratedCoffee (with milk or sugar) can be used anywhere a Coffee is expected, as long as the decorator correctly implements the interface. However, care must be taken to ensure decorators don't introduce unexpected behaviour that may break contracts.
D - Dependency Inversion: Both base and decorators depend on an abstraction, not each other's implementation. Both the base component and decorators depend on the same abstract interface, allowing flexibility in how components are composed. For example a Coffee implementation, whether it's a base Espresso or a decorated ExpressoWithMilk.

Basic Example (PHP)

Here is a lightweight PHP example:

interface Logger {
    public function log(string $message): void;
}

class FileLogger implements Logger {
    public function log(string $message): void {
        echo "Logging: {$message}\n";
    }
}

abstract class LoggerDecorator implements Logger {
    protected Logger $logger;

    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }

    public function log(string $message): void {
        $this->logger->log($message);
    }
}

class TimestampLogger extends LoggerDecorator {
    public function log(string $message): void {
        parent::log("[" . date('Y-m-d H:i:s') . "] " . $message);
    }
}

class EncryptLogger extends LoggerDecorator {
    public function log(string $message): void {
        parent::log(base64_encode($message));
    }
}

$logger = new EncryptLogger(new TimestampLogger(new FileLogger()));
$logger->log("User logged in");
Enter fullscreen mode Exit fullscreen mode

Output:

Logging: [2025-10-18 21:00:00] VXNlciBsb2dnZWQgaW4=
Enter fullscreen mode Exit fullscreen mode

Class Diagram - Logger

The diagram below visualizes the structure of the Decorator Pattern in our above example.

  • Logger defines the core interface.
  • FileLogger implements the interface directly.
  • LoggerDecorator wraps any Logger instance.
  • Concrete decorators (TimestampLogger, EncryptLogger) extend the decorator to add new behaviour.

Real-Life Use Case in TypeScript: An Exensible API Client

Now, let's build something that can be used in production, an API Request Handler that can dynamically gain logging, caching or retry capabilities.

Step 1: Define an Interface that all API clients will implement.

interface ApiClient {
  request(url: string): Promise<any>;
} 
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Base HTTP Client

class HttpClient implements ApiClient {
  async request(url: string): Promise<any> {
    console.log(`Fetching from: ${url}`);
    const response = await fetch(url);
    return response.json();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create an Abstract Decorator that each decorator will extend

abstract class ApiClientDecorator implements ApiClient {
  constructor(protected client: ApiClient) {}

  request(url: string): Promise<any> {
    return this.client.request(url);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Add Behaviour-Specific Decorators

LoggingDecorator

class LoggingDecorator extends ApiClientDecorator {
  async request(url: string): Promise<any> {
    console.log(`[LOG] Starting request: ${url}`);
    const result = await super.request(url);
    console.log(`[LOG] Completed request: ${url}`);
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

CachingDecorator

class CachingDecorator extends ApiClientDecorator {
  private cache = new Map<string, any>();

  async request(url: string): Promise<any> {
    if (this.cache.has(url)) {
      console.log(`[CACHE HIT] ${url}`);
      return this.cache.get(url);
    }

    console.log(`[CACHE MISS] ${url}`);
    const result = await super.request(url);
    this.cache.set(url, result);
    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

RetryDecorator

class RetryDecorator extends ApiClientDecorator {
  constructor(client: ApiClient, private retries = 3) {
    super(client);
  }

  async request(url: string): Promise<any> {
    for (let attempt = 1; attempt <= this.retries; attempt++) {
      try {
        return await super.request(url);
      } catch (err) {
        console.warn(`[RETRY] Attempt ${attempt} failed`);
        if (attempt === this.retries) throw err;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Compose the Decorators

async function main() {
  let client: ApiClient = new HttpClient();

  // Add decorators as layers
  client = new RetryDecorator(
              new CachingDecorator(
                new LoggingDecorator(client)
              )
            );

  const data1 = await client.request("https://jsonplaceholder.typicode.com/users/1");
  const data2 = await client.request("https://jsonplaceholder.typicode.com/users/1"); // Cached

  console.log("User:", data1.name);
}

main();
Enter fullscreen mode Exit fullscreen mode

Output

[LOG] Starting request: https://jsonplaceholder.typicode.com/users/1
[CACHE MISS] https://jsonplaceholder.typicode.com/users/1
[LOG] Completed request: https://jsonplaceholder.typicode.com/users/1
[CACHE HIT] https://jsonplaceholder.typicode.com/users/1
User: James Anderson
Enter fullscreen mode Exit fullscreen mode

Class Diagram - API Client

The diagram below shows the layering of decorators around the base HttpClient. Each decorator(Logging, Caching, Retry) wraps another, forming a stack of added behaviour that enhances the base client dynamically.

Advantages & Disadvantages

Advantages Disadvantages
Flexible runtime composition Can introduce complexity with many layers
Promotes reusability Harder to debug due to nested wrappers
Adheres to SOLID Order of decorators affects behaviour
Avoids deep inheritance Requires more setup code

When to Use

  • You need optional features (e.g. caching)
  • You want to add behaviour without subclassing.
  • You want to compose behaviours at runtime.

Conclusion

The Decorator Pattern is one of the most flexible tools in a developer's design arsenal. It elegantly combines composition over inheritance with SOLID principles, allowing you to add features on the fly without breaking your existing code.

Whether you're writing PHP backends, TypeScript services or you're working with Java, you're probably using decorators already, maybe without realizing it.

Cover Image: "Cafe Adventure" by Andrew Tanglao

Top comments (0)