Skip to main content

Chain of Responsibility Pattern

Route requests through a chain of handlers, each processing or delegating

TL;DR

Chain of Responsibility decouples request senders from handlers by passing requests along a dynamic chain. Each handler decides whether to process the request or pass it to the next handler. Use when multiple objects might handle a request, the handler is unknown at compile time, or you want to avoid hard-coded dependencies.

Learning Objectives

  • You will understand when requests should be routed through a chain of handlers.
  • You will identify the structure of handler chains and how handlers interconnect.
  • You will implement handlers that process or delegate requests appropriately.
  • You will design chains that are flexible and maintainable without tight coupling.

Motivating Scenario

A web application receives HTTP requests. Different middleware components need to process them: authentication, logging, compression, request validation. Adding a new middleware shouldn't require modifying existing code. Chain of Responsibility lets each middleware check the request, process it, and pass it to the next handler—building a clean pipeline without knowing which handlers follow.

Core Concepts

Chain of Responsibility organizes handlers in a sequence where each can process a request or pass it along. Senders don't know which handler will ultimately process the request—they just submit it to the chain.

Key elements:

  • Handler: interface defining the method to handle requests and a reference to the next handler
  • ConcreteHandler: implements the handler interface, processing requests or delegating
  • Client: creates the chain and sends requests to the first handler
Chain of Responsibility structure

Practical Example

Consider a support ticket system where requests are routed to handlers with increasing authority.

chain_of_responsibility.py
from abc import ABC, abstractmethod
from typing import Optional

class SupportRequest:
def __init__(self, ticket_id: str, priority: int, issue: str):
self.ticket_id = ticket_id
self.priority = priority
self.issue = issue
self.resolved = False

class Handler(ABC):
def __init__(self):
self._next_handler: Optional[Handler] = None

def set_next(self, handler: 'Handler') -> 'Handler':
self._next_handler = handler
return handler

@abstractmethod
def handle(self, request: SupportRequest) -> None:
pass

class Level1Support(Handler):
def handle(self, request: SupportRequest) -> None:
if request.priority <= 2:
print(f"L1 resolved ticket {request.ticket_id}: {request.issue}")
request.resolved = True
elif self._next_handler:
self._next_handler.handle(request)

class Level2Support(Handler):
def handle(self, request: SupportRequest) -> None:
if request.priority <= 4:
print(f"L2 resolved ticket {request.ticket_id}: {request.issue}")
request.resolved = True
elif self._next_handler:
self._next_handler.handle(request)

class Level3Support(Handler):
def handle(self, request: SupportRequest) -> None:
print(f"L3 escalated ticket {request.ticket_id}: {request.issue}")
request.resolved = True

# Usage
level1 = Level1Support()
level2 = Level2Support()
level3 = Level3Support()

level1.set_next(level2).set_next(level3)

requests = [
SupportRequest("T001", 1, "Password reset"),
SupportRequest("T002", 3, "Database performance issue"),
SupportRequest("T003", 5, "System architecture redesign"),
]

for req in requests:
level1.handle(req)

When to Use / When Not to Use

Use Chain of Responsibility
  1. Multiple handlers might process a single request
  2. Handlers are determined dynamically at runtime
  3. You want to add or remove handlers without modifying client code
  4. Processing pipeline: middleware, event handling, request routing
  5. Handler responsibility is unclear or context-dependent
Avoid Chain of Responsibility
  1. Single, fixed handler always processes requests
  2. Handler mapping is known and stable at compile time
  3. Performance is critical and you want direct method calls
  4. Chain depth might be extreme, causing latency
  5. You need synchronous request/response patterns only

Patterns and Pitfalls

Design Review Checklist

  • Is the chain of handlers built and configured correctly before processing requests?
  • Does each handler have a clear, well-defined responsibility?
  • Are unhandled requests properly managed (logged, rejected, or escalated)?
  • Can handlers be dynamically added, removed, or reordered without breaking the system?
  • Is the request state protected from unexpected mutations by handlers?
  • Do handlers avoid infinite loops or circular dependencies?
  • Is the chain depth monitored to prevent excessive latency?

Self-Check

  1. How does Chain of Responsibility differ from a simple if-else cascade for routing requests? The pattern provides dynamic, runtime configuration of handlers without coupling senders to specific handler types.

  2. What happens if no handler in the chain processes the request? Design chains with a default handler, or ensure requests are logged or rejected safely.

  3. Can handlers modify the request and pass it along? Yes—handlers can enrich or transform requests, but this must be coordinated to avoid conflicts.

One Takeaway

Chain of Responsibility transforms rigid, compile-time request routing into flexible, runtime-configurable pipelines. Use it when handlers are uncertain or dynamic—especially in middleware, event processing, and hierarchical escalation systems.

Next Steps

References