Skip to contentJMRG
All posts

Decorator Pattern: Middleware and Dynamic Behavior

design-patternsstructuraldecorator

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--> Component

When 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:

  1. CacheDecorator: Checks cache, returns if hit
  2. AuthDecorator: Adds Authorization header
  3. LoggingDecorator: Logs start, timing
  4. BasicHttpClient: Executes actual request
  5. LoggingDecorator: Logs result
  6. 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).