Decorator Pattern: Middleware and Dynamic Behavior

What is the Decorator Pattern?
The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to inheritance for extending functionality, allowing behaviors to be combined modularly without modifying the original code.
classDiagram
class Component {
<<interface>>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
<<abstract>>
-component: Component
+operation()
}
class ConcreteDecoratorA {
+operation()
+addedBehavior()
}
class ConcreteDecoratorB {
+operation()
+addedState
}
Component <|.. ConcreteComponent
Component <|.. Decorator
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
Decorator o--> ComponentWhen to Use Decorator
- Add behavior to objects without modifying their code
- When extension via inheritance isn't practical
- Middleware, logging, caching, validation
- Dynamic combination of behaviors
Step by Step Implementation
1. Define the Component Interface
The HttpClient interface defines the contract that both the base component and decorators will implement:
interface HttpRequest {
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
}
interface HttpResponse<T> {
data: T;
status: number;
headers: Record<string, string>;
duration?: number;
}
interface HttpClient {
request<T>(req: HttpRequest): Promise<HttpResponse<T>>;
}2. Implement the Concrete Component
BasicHttpClient is the base implementation without additional features:
class BasicHttpClient implements HttpClient {
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
const response = await fetch(req.url, {
method: req.method,
headers: req.headers,
body: req.body ? JSON.stringify(req.body) : undefined,
});
return {
data: await response.json(),
status: response.status,
headers: Object.fromEntries(response.headers),
};
}
}3. Create the Abstract Base Decorator
All concrete decorators inherit from this class:
abstract class HttpClientDecorator implements HttpClient {
constructor(protected wrapped: HttpClient) {}
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
return this.wrapped.request<T>(req);
}
}4. Implement Concrete Decorators
Each decorator adds a specific functionality:
/** Decorator that logs requests and response times */
class LoggingDecorator extends HttpClientDecorator {
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
const startTime = Date.now();
console.log(`[HTTP] -> ${req.method} ${req.url}`);
const response = await this.wrapped.request<T>(req);
const duration = Date.now() - startTime;
console.log(`[HTTP] <- ${response.status} (${duration}ms)`);
return { ...response, duration };
}
}
/** Decorator that adds authentication */
class AuthDecorator extends HttpClientDecorator {
constructor(
wrapped: HttpClient,
private getToken: () => string
) {
super(wrapped);
}
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
const token = this.getToken();
const headers = {
...req.headers,
Authorization: `Bearer ${token}`,
};
return this.wrapped.request<T>({ ...req, headers });
}
}
/** Decorator that implements cache for GET requests */
class CacheDecorator extends HttpClientDecorator {
private cache = new Map<string, { data: unknown; expiry: number }>();
constructor(
wrapped: HttpClient,
private ttl: number
) {
super(wrapped);
}
async request<T>(req: HttpRequest): Promise<HttpResponse<T>> {
if (req.method !== 'GET') {
return this.wrapped.request<T>(req);
}
const cached = this.cache.get(req.url);
if (cached && cached.expiry > Date.now()) {
console.log(`[CACHE] HIT: ${req.url}`);
return cached.data as HttpResponse<T>;
}
const response = await this.wrapped.request<T>(req);
this.cache.set(req.url, {
data: response,
expiry: Date.now() + this.ttl,
});
return response;
}
}5. Compose Decorators
Decorators are stacked in order (order matters):
// Base client
let client: HttpClient = new BasicHttpClient();
// Apply decorators from inside out
client = new LoggingDecorator(client);
client = new AuthDecorator(client, () => 'my-token');
client = new CacheDecorator(client, 60000);
// Use as normal client
const response = await client.request<User[]>({
method: 'GET',
url: '/api/users',
});Execution Flow
When request is called:
- CacheDecorator: Checks cache, returns if hit
- AuthDecorator: Adds Authorization header
- LoggingDecorator: Logs start, timing
- BasicHttpClient: Executes actual request
- LoggingDecorator: Logs result
- CacheDecorator: Stores in cache
Available Decorators
| Decorator | Functionality |
|---|---|
LoggingDecorator |
Logs requests and times |
AuthDecorator |
Adds authentication token |
CacheDecorator |
Caches GET responses with TTL |
RetryDecorator |
Retries with exponential backoff |
TimeoutDecorator |
Cancels slow requests |
Summary
| Aspect | Description |
|---|---|
| Purpose | Add behavior dynamically without modifying classes |
| Components | Component, ConcreteComponent, Decorator, ConcreteDecorators |
| Advantage | Flexible composition, Open/Closed principle |
| Disadvantage | Many small objects, decorator order matters |
Conclusion
The Decorator pattern is ideal for middleware and cross-cutting concerns. It allows adding features like logging, caching, and authentication in a modular and reusable way.
In the next article we'll explore the Facade Pattern, which simplifies access to complex systems.
Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).