StackPractices
intermediate By Mathias Paulenko

Chain of Responsibility Pattern

Pass requests along a chain of handlers until one handles it. A behavioral design pattern for decoupling senders and receivers.

Topics: design

Overview

The Chain of Responsibility Pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain. This decouples senders from receivers and allows multiple objects to handle a request without the sender knowing which one will process it.

When to Use

Use the Chain of Responsibility Pattern when:

  • More than one object may handle a request, and the handler isn’t known in advance
  • You want to issue a request to one of several objects without specifying the receiver explicitly
  • The set of objects that can handle a request should be specified dynamically
  • You need a pipeline or middleware where each step can process, transform, or stop a request

Solution

Python

from abc import ABC, abstractmethod
from typing import Optional

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

    def set_next(self, handler: 'Handler') -> 'Handler':
        self._next = handler
        return handler  # Enable fluent chaining

    @abstractmethod
    def handle(self, request: str) -> Optional[str]:
        pass

    def _pass_to_next(self, request: str) -> Optional[str]:
        if self._next:
            return self._next.handle(request)
        return None

class AuthHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        if not request.startswith("token:"):
            return "401 Unauthorized"
        return self._pass_to_next(request)

class RateLimitHandler(Handler):
    def __init__(self):
        super().__init__()
        self.requests = 0
        self.limit = 3

    def handle(self, request: str) -> Optional[str]:
        self.requests += 1
        if self.requests > self.limit:
            return "429 Too Many Requests"
        return self._pass_to_next(request)

class DataHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        return f"Processed: {request}"

# Build the chain
handler = AuthHandler()
handler.set_next(RateLimitHandler()).set_next(DataHandler())

print(handler.handle("token:abc123"))  # Processed
print(handler.handle("bad-request"))    # 401 Unauthorized

JavaScript

class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    return null;
  }
}

class AuthHandler extends Handler {
  handle(request) {
    if (!request.startsWith("token:")) {
      return "401 Unauthorized";
    }
    return super.handle(request);
  }
}

class RateLimitHandler extends Handler {
  constructor() {
    super();
    this.requests = 0;
    this.limit = 3;
  }

  handle(request) {
    this.requests++;
    if (this.requests > this.limit) {
      return "429 Too Many Requests";
    }
    return super.handle(request);
  }
}

class DataHandler extends Handler {
  handle(request) {
    return `Processed: ${request}`;
  }
}

// Build the chain
const handler = new AuthHandler();
handler.setNext(new RateLimitHandler()).setNext(new DataHandler());

console.log(handler.handle("token:abc123")); // Processed
console.log(handler.handle("bad-request"));     // 401

Java

public abstract class Handler {
    protected Handler next;

    public Handler setNext(Handler next) {
        this.next = next;
        return next;
    }

    public abstract String handle(String request);

    protected String passToNext(String request) {
        if (next != null) {
            return next.handle(request);
        }
        return null;
    }
}

public class AuthHandler extends Handler {
    @Override
    public String handle(String request) {
        if (!request.startsWith("token:")) {
            return "401 Unauthorized";
        }
        return passToNext(request);
    }
}

public class RateLimitHandler extends Handler {
    private int requests = 0;
    private final int limit = 3;

    @Override
    public String handle(String request) {
        requests++;
        if (requests > limit) {
            return "429 Too Many Requests";
        }
        return passToNext(request);
    }
}

public class DataHandler extends Handler {
    @Override
    public String handle(String request) {
        return "Processed: " + request;
    }
}

// Build the chain
Handler handler = new AuthHandler();
handler.setNext(new RateLimitHandler()).setNext(new DataHandler());

System.out.println(handler.handle("token:abc")); // Processed
System.out.println(handler.handle("bad"));        // 401

Explanation

The Chain of Responsibility Pattern has two roles:

  • Handler Interface — declares a handle() method and maintains a reference to the next handler
  • Concrete Handlers — implement processing logic; each decides whether to handle the request or pass it along

The client builds the chain order. Each handler can also choose to stop the chain (short-circuit) by returning early without calling the next handler.

Variants

VariantStructureBest For
Linear ChainLinked list of handlersSimple sequential processing
Tree ChainHandlers organized hierarchicallyMulti-level decision trees
Middleware PipelineArray of functions, each calls next()Web frameworks (Express, Django middleware)
Event BusHandlers register for specific eventsDecoupled event-driven systems

What Works

  • Keep handlers focused — each handler should do one thing (auth, validation, logging, etc.)
  • Provide a default handler at the end of the chain to avoid unhandled requests
  • Allow chain modification at runtime by exposing setNext() or addHandler() methods
  • Use immutable request objects so handlers don’t accidentally modify shared state
  • Consider order carefully — handlers that short-circuit (auth, rate limiting) should typically come first

Common Mistakes

  • Creating circular chains where a handler eventually calls itself, causing infinite loops
  • Forgetting to call the next handler, silently dropping requests that should have been processed
  • Putting slow or blocking handlers early in the chain, causing unnecessary latency for rejected requests
  • Storing mutable state in handlers that are reused across requests, causing cross-request contamination
  • Building overly long chains that become hard to debug and reason about
  • Not providing a default handler at the end of the chain, leaving requests unhandled
  • Mixing concerns within a single handler instead of keeping them focused
  • Not documenting handler dependencies and expected order
  • Failing to handle exceptions in handlers, causing the entire chain to fail
  • Using the chain pattern when a simple conditional would suffice

Best Practices

  1. Keep handlers single-responsibility. Each handler should handle one specific concern (auth, validation, logging, etc.) to maintain clarity and testability.

  2. Provide a default handler. Always include a catch-all handler at the end of the chain to handle requests that fall through, preventing silent failures.

  3. Document handler order. Clearly document the expected order of handlers and any dependencies between them, as order considerably affects behavior.

  4. Use immutable request objects. Pass immutable request objects through the chain to prevent handlers from accidentally modifying shared state.

  5. Handle exceptions gracefully. Each handler should catch and handle its own exceptions, or wrap them appropriately to prevent chain failure.

  6. Consider performance impact. Place fast, short-circuiting handlers (auth, rate limiting) early in the chain to fail fast and avoid unnecessary processing.

  7. Support chain reconfiguration. Allow handlers to be added, removed, or reordered at runtime for flexibility.

  8. Add logging and monitoring. Include logging handlers to trace request flow through the chain and identify bottlenecks or failures.

  9. Avoid circular references. Ensure the chain structure is acyclic to prevent infinite loops during request processing.

  10. Test handlers in isolation. Write unit tests for each handler independently, then integration tests for the complete chain.

Frequently Asked Questions

Is this the same as middleware in web frameworks?

Yes. Express.js middleware, Django middleware, and ASP.NET Core middleware are all implementations of the Chain of Responsibility Pattern. Each middleware decides whether to process the request, pass it along, or short-circuit with a response.

What happens if no handler processes the request?

By default, the request falls through the chain unhandled. Consider adding a Decorator for enrichment or a default handler for catch-all behavior. You should add a catch-all handler at the end that returns a default response or throws an appropriate error.

Can handlers modify the request before passing it along?

Yes. Unlike a pure pass-or-fail chain, handlers can transform, enrich, or validate the request before forwarding it. This is common in middleware pipelines.

How do I handle async operations in a chain?

Use async/await patterns where each handler returns a promise. The chain awaits each handler's completion before passing to the next. This is essential for I/O-bound operations like database queries or API calls.

Should I use this pattern for event handling?

Yes. Event systems often use a variation of this pattern where multiple event listeners (handlers) can process an event. Unlike a linear chain, event systems typically broadcast to all registered handlers.

How do I add logging to trace request flow?

Add a logging handler that records the request before and after each handler. Include timestamps and handler names to trace the execution path and identify performance bottlenecks.

Can handlers have side effects?

Yes, handlers can perform side effects like logging, metrics collection, or database updates. However, keep side effects focused and document them clearly to avoid unexpected behavior.

How do I handle timeout scenarios in a chain?

Implement timeout middleware that tracks request duration and short-circuits if processing exceeds a threshold. This prevents slow handlers from blocking the entire chain indefinitely.

Should handlers be stateless?

Ideally yes. Stateless handlers are easier to test and reuse. If state is necessary, ensure it's request-scoped (stored in the context object) rather than handler-scoped to avoid cross-request contamination.

How do I implement circuit breaking in a chain?

Add a circuit breaker handler that tracks failure rates and short-circuits requests when a downstream service is failing. This prevents cascading failures and improves system resilience.