"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");
Output:
Logging: [2025-10-18 21:00:00] VXNlciBsb2dnZWQgaW4=
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>;
}
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();
}
}
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);
}
}
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;
}
}
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;
}
}
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;
}
}
}
}
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();
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
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)