Browse TypeScript Design Patterns & Application Architecture

Chain of Responsibility Pattern: Use Cases and Examples

Explore practical applications of the Chain of Responsibility Pattern in TypeScript, including middleware pipelines, event handling systems, and support ticket escalation.

6.1.3 Use Cases and Examples

The Chain of Responsibility Pattern is a behavioral design pattern that allows an object to pass a request along a chain of potential handlers until one of them handles the request. This pattern promotes loose coupling in the system by allowing multiple objects to handle a request without the sender needing to know which object will handle it. Let’s delve into some practical scenarios where this pattern can be effectively applied, including middleware pipelines, event handling systems, and support ticket escalation.

Implementing a Web Server Middleware Pipeline

One of the most common use cases for the Chain of Responsibility Pattern is in building middleware pipelines for web servers. Middleware functions are components that process requests and responses in a web application. They can perform tasks such as authentication, logging, and error handling.

Example: Middleware Pipeline in TypeScript

Let’s implement a simple middleware pipeline using the Chain of Responsibility Pattern in TypeScript. We’ll create a series of middleware functions that process HTTP requests.

 1// Define the Request and Response types
 2interface Request {
 3  url: string;
 4  headers: Record<string, string>;
 5  body: any;
 6}
 7
 8interface Response {
 9  statusCode: number;
10  body: any;
11}
12
13// Middleware interface
14interface Middleware {
15  setNext(middleware: Middleware): Middleware;
16  handle(request: Request, response: Response): void;
17}
18
19// Abstract Middleware class
20abstract class AbstractMiddleware implements Middleware {
21  private nextMiddleware: Middleware | null = null;
22
23  setNext(middleware: Middleware): Middleware {
24    this.nextMiddleware = middleware;
25    return middleware;
26  }
27
28  handle(request: Request, response: Response): void {
29    if (this.nextMiddleware) {
30      this.nextMiddleware.handle(request, response);
31    }
32  }
33}
34
35// Concrete Middleware: Authentication
36class AuthenticationMiddleware extends AbstractMiddleware {
37  handle(request: Request, response: Response): void {
38    if (!request.headers['Authorization']) {
39      response.statusCode = 401;
40      response.body = 'Unauthorized';
41      return;
42    }
43    console.log('Authentication successful');
44    super.handle(request, response);
45  }
46}
47
48// Concrete Middleware: Logging
49class LoggingMiddleware extends AbstractMiddleware {
50  handle(request: Request, response: Response): void {
51    console.log(`Request URL: ${request.url}`);
52    super.handle(request, response);
53  }
54}
55
56// Concrete Middleware: Error Handling
57class ErrorHandlingMiddleware extends AbstractMiddleware {
58  handle(request: Request, response: Response): void {
59    try {
60      super.handle(request, response);
61    } catch (error) {
62      response.statusCode = 500;
63      response.body = 'Internal Server Error';
64      console.error('Error:', error);
65    }
66  }
67}
68
69// Client code
70const request: Request = {
71  url: '/api/data',
72  headers: { 'Authorization': 'Bearer token' },
73  body: {}
74};
75
76const response: Response = {
77  statusCode: 200,
78  body: {}
79};
80
81// Set up the middleware chain
82const authMiddleware = new AuthenticationMiddleware();
83const logMiddleware = new LoggingMiddleware();
84const errorMiddleware = new ErrorHandlingMiddleware();
85
86authMiddleware.setNext(logMiddleware).setNext(errorMiddleware);
87
88// Process the request
89authMiddleware.handle(request, response);
90
91console.log('Response:', response);

In this example, we define a series of middleware classes that extend an abstract middleware class. Each middleware can handle a request and pass it to the next middleware in the chain. This setup allows us to add, remove, or reorder middleware components without changing the client code.

Benefits of Middleware Pipelines

  • Maintainability: Middleware components are modular and can be developed and tested independently.
  • Extensibility: New middleware can be added to the pipeline without modifying existing components.
  • Separation of Concerns: Each middleware has a specific responsibility, promoting clean and organized code.

Building an Event Handling System

Another effective application of the Chain of Responsibility Pattern is in event handling systems. In such systems, different handlers respond to events based on their type or content.

Example: Event Handling in TypeScript

Consider an event handling system where different handlers process events based on their type.

 1// Define the Event type
 2interface Event {
 3  type: string;
 4  payload: any;
 5}
 6
 7// Event Handler interface
 8interface EventHandler {
 9  setNext(handler: EventHandler): EventHandler;
10  handle(event: Event): void;
11}
12
13// Abstract Event Handler class
14abstract class AbstractEventHandler implements EventHandler {
15  private nextHandler: EventHandler | null = null;
16
17  setNext(handler: EventHandler): EventHandler {
18    this.nextHandler = handler;
19    return handler;
20  }
21
22  handle(event: Event): void {
23    if (this.nextHandler) {
24      this.nextHandler.handle(event);
25    }
26  }
27}
28
29// Concrete Event Handler: Login Event
30class LoginEventHandler extends AbstractEventHandler {
31  handle(event: Event): void {
32    if (event.type === 'login') {
33      console.log('Handling login event:', event.payload);
34      return;
35    }
36    super.handle(event);
37  }
38}
39
40// Concrete Event Handler: Logout Event
41class LogoutEventHandler extends AbstractEventHandler {
42  handle(event: Event): void {
43    if (event.type === 'logout') {
44      console.log('Handling logout event:', event.payload);
45      return;
46    }
47    super.handle(event);
48  }
49}
50
51// Concrete Event Handler: Default Event
52class DefaultEventHandler extends AbstractEventHandler {
53  handle(event: Event): void {
54    console.log('Unhandled event type:', event.type);
55  }
56}
57
58// Client code
59const loginHandler = new LoginEventHandler();
60const logoutHandler = new LogoutEventHandler();
61const defaultHandler = new DefaultEventHandler();
62
63loginHandler.setNext(logoutHandler).setNext(defaultHandler);
64
65// Process an event
66const event: Event = { type: 'login', payload: { user: 'Alice' } };
67loginHandler.handle(event);

In this example, we create a chain of event handlers, each responsible for handling a specific type of event. If a handler cannot process an event, it passes the event to the next handler in the chain.

Benefits of Event Handling Systems

  • Flexibility: Handlers can be added or removed without affecting other parts of the system.
  • Decoupling: The system is not tightly coupled to specific event types, allowing for easy modification and extension.
  • Responsibility Distribution: Each handler has a clear responsibility, making the system easier to understand and maintain.

Creating a Support Ticket System

The Chain of Responsibility Pattern is also useful in creating systems where requests need to escalate through different levels of processing, such as a support ticket system.

Example: Support Ticket Escalation in TypeScript

Let’s implement a support ticket system where requests escalate through support tiers until resolved.

 1// Define the Support Ticket type
 2interface SupportTicket {
 3  id: number;
 4  issue: string;
 5  severity: string;
 6}
 7
 8// Support Handler interface
 9interface SupportHandler {
10  setNext(handler: SupportHandler): SupportHandler;
11  handle(ticket: SupportTicket): void;
12}
13
14// Abstract Support Handler class
15abstract class AbstractSupportHandler implements SupportHandler {
16  private nextHandler: SupportHandler | null = null;
17
18  setNext(handler: SupportHandler): SupportHandler {
19    this.nextHandler = handler;
20    return handler;
21  }
22
23  handle(ticket: SupportTicket): void {
24    if (this.nextHandler) {
25      this.nextHandler.handle(ticket);
26    }
27  }
28}
29
30// Concrete Support Handler: Tier 1 Support
31class Tier1SupportHandler extends AbstractSupportHandler {
32  handle(ticket: SupportTicket): void {
33    if (ticket.severity === 'low') {
34      console.log('Tier 1 handling ticket:', ticket.id);
35      return;
36    }
37    super.handle(ticket);
38  }
39}
40
41// Concrete Support Handler: Tier 2 Support
42class Tier2SupportHandler extends AbstractSupportHandler {
43  handle(ticket: SupportTicket): void {
44    if (ticket.severity === 'medium') {
45      console.log('Tier 2 handling ticket:', ticket.id);
46      return;
47    }
48    super.handle(ticket);
49  }
50}
51
52// Concrete Support Handler: Tier 3 Support
53class Tier3SupportHandler extends AbstractSupportHandler {
54  handle(ticket: SupportTicket): void {
55    console.log('Tier 3 handling ticket:', ticket.id);
56  }
57}
58
59// Client code
60const tier1Handler = new Tier1SupportHandler();
61const tier2Handler = new Tier2SupportHandler();
62const tier3Handler = new Tier3SupportHandler();
63
64tier1Handler.setNext(tier2Handler).setNext(tier3Handler);
65
66// Process a support ticket
67const ticket: SupportTicket = { id: 101, issue: 'Network issue', severity: 'medium' };
68tier1Handler.handle(ticket);

In this example, we create a chain of support handlers, each responsible for handling tickets of a specific severity. If a handler cannot resolve a ticket, it escalates the ticket to the next handler in the chain.

Benefits of Support Ticket Systems

  • Scalability: The system can handle an increasing number of tickets by adding more handlers.
  • Efficiency: Tickets are processed by the appropriate handler, reducing response time.
  • Clear Escalation Path: The chain provides a clear path for ticket escalation, ensuring that issues are addressed at the right level.

Addressing System Needs with the Chain of Responsibility Pattern

The Chain of Responsibility Pattern addresses several key needs in these systems:

  • Decoupling: By separating the sender of a request from its receivers, the pattern reduces dependencies and enhances flexibility.
  • Dynamic Handling: Requests can be processed dynamically by different handlers, allowing for adaptable and responsive systems.
  • Responsibility Segmentation: Each handler has a specific responsibility, promoting clean and maintainable code.

Potential Drawbacks and Considerations

While the Chain of Responsibility Pattern offers many benefits, there are potential drawbacks to consider:

  • Chain Overhead: The pattern may introduce overhead if the chain is long or if requests frequently traverse the entire chain without being handled.
  • Debugging Challenges: Debugging can be more complex, as it may be difficult to trace which handler processed a request.
  • Responsibility Confusion: If not carefully managed, it can be unclear which handler is responsible for processing a request.

Encouragement to Use the Pattern

Consider using the Chain of Responsibility Pattern when you need a flexible and decoupled mechanism for processing requests. This pattern is particularly useful in systems where requests need to be handled by multiple components, such as middleware pipelines, event handling systems, and support ticket escalation.

Try It Yourself

Experiment with the provided code examples by adding new middleware, event handlers, or support tiers. Consider how the system behaves with different configurations and explore the flexibility offered by the Chain of Responsibility Pattern.

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026