Chain of Responsibility Pattern: Passing the Request Along the Chain
By Misbahul Munir4 min read801 words

Chain of Responsibility Pattern: Passing the Request Along the Chain

Backend Development
design pattern
behavioral pattern

Sometimes in backend systems, a request might have multiple possible handlers, and we don’t want the sender to know which one will actually process it.

Instead of hardcoding if-else or switch statements everywhere, we can pass the request along a chain of handlers until one takes responsibility.

That’s exactly what the Chain of Responsibility Pattern is designed for.


What is the Chain of Responsibility Pattern?

The Chain of Responsibility pattern lets you pass a request through a chain of potential handlers.

Each handler decides either to:

  1. Process the request, or
  2. Pass it on to the next handler in the chain.

This approach decouples the sender from the specific receiver, making the system more flexible and easier to extend.


Real-World Analogy: Customer Service Escalation

Think about calling customer service:

  1. You start with a tier 1 support agent.
  2. If they can’t solve your issue, they escalate it to tier 2.
  3. If tier 2 still can’t resolve it, it goes to a manager.

The customer doesn’t know or care who exactly solves the problem — they just make the request and let the chain handle it.


When to Use It in Backend Development

You might use the Chain of Responsibility pattern when:

  • You have a pipeline of processing steps where only some steps may apply.
  • You need to dynamically change or extend the set of handlers.
  • You want to avoid big if-else or switch statements in request processing.

Common backend use cases:

  • Middleware pipelines (like Flask, Django, or FastAPI request processing).
  • Authorization checks with multiple fallback levels.
  • Message validation pipelines in event-driven systems.

Example: Chain of Responsibility for API Request Validation

Let’s simulate an API request going through a chain of validators:

Step 1: Handler Interface

from abc import ABC, abstractmethod class Handler(ABC): def __init__(self): self._next_handler = None def set_next(self, handler): self._next_handler = handler return handler # allows chaining @abstractmethod def handle(self, request): pass def next(self, request): if self._next_handler: return self._next_handler.handle(request) return None

Step 2: Concrete Handlers

class AuthHandler(Handler): def handle(self, request): if not request.get("user"): return "Authentication Failed" print("[AuthHandler] User authenticated") return self.next(request) class RoleHandler(Handler): def handle(self, request): if request.get("role") != "admin": return "Authorization Failed" print("[RoleHandler] User authorized as admin") return self.next(request) class DataValidationHandler(Handler): def handle(self, request): if not request.get("data"): return "Data Validation Failed" print("[DataValidationHandler] Data is valid") return self.next(request)

Step 3: Putting It All Together

if __name__ == "__main__": # Build the chain auth = AuthHandler() role = RoleHandler() data_validation = DataValidationHandler() auth.set_next(role).set_next(data_validation) # Example request request = { "user": "munir", "role": "admin", "data": {"field": "value"} } result = auth.handle(request) print("Result:", result or "Request processed successfully")

Output:

[AuthHandler] User authenticated [RoleHandler] User authorized as admin [DataValidationHandler] Data is valid Result: Request processed successfully

Why It Works

  • Loose Coupling: The sender doesn’t know which handler will process the request.
  • Flexibility: You can change the chain order or add/remove handlers without touching the request code.
  • Reusability: Handlers can be reused in different chains.

Real Backend Use Case: Django Middleware

In Django, HTTP requests pass through a middleware chain before reaching the view.

Each middleware can process the request, modify it, or return a response — or pass it along to the next middleware in the chain.

Example:

class AuthenticationMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if not request.user.is_authenticated: return HttpResponse("Unauthorized", status=401) return self.get_response(request)

This is essentially a Chain of Responsibility implementation.


Gotchas and Considerations

1. Chain Order Matters

Handlers execute in the order they are linked, so changing the sequence may cause unexpected results.

Always document or test chain configurations.


2. Request Might Never Get Handled

If no handler processes the request, it might be silently ignored.

Add a fallback handler at the end that logs or returns a default response.


3. Debugging Can Be Tricky

Requests might pass through many handlers before failing.

Use logging or tracing to monitor handler execution order.


4. Performance Impact

Long chains can add latency, especially if handlers do network or I/O operations.

Keep chains short and move heavy work to background jobs if possible.


5. Tight Coupling of Request Format

If all handlers assume the request has certain fields, changing the request structure can break the whole chain.

Use a well-defined request schema or data class.


Conclusion

The Chain of Responsibility Pattern shines when you want flexible, decoupled request processing pipelines.

It’s the foundation behind many backend frameworks’ middleware systems and can help you avoid deeply nested conditional logic.

In the next part of the Behavioral series, we’ll explore the Mediator Pattern, which centralizes communication between components.