designpattern.site

Chain of Responsibility Pattern

Pass a request along a chain of handlers, where each handler decides to process the request or forward it to the next handler in the chain.

Overview

Most real-world request processing is not a single decision — it is a series of checks, transformations, or escalations. An HTTP request passes through authentication, rate-limiting, logging, and routing middleware before it ever reaches your business logic. A customer support ticket moves from a front-line agent to a specialist to a manager when earlier handlers cannot resolve it.

The Chain of Responsibility pattern is a behavioral design pattern that models this naturally. It decouples the sender of a request from the receivers by giving multiple objects the chance to handle the request. Each handler in the chain either handles the request or passes it to the next handler — the sender never needs to know which handler ultimately does the work.

This pattern excels when the set of handlers and their order needs to change independently of the code that sends requests, and when you want to avoid hard-coding complex conditional logic in a single place.

Problem & Motivation

Imagine you are building an HTTP middleware pipeline for a web server. Every incoming request must pass through:

  1. Authentication — reject requests with invalid tokens immediately
  2. Rate limiting — block clients that exceed their quota
  3. Request logging — record all requests that get this far
  4. Business logic — the actual handler for the route

Without a structured pattern you end up with nested if statements, all tangled together inside one enormous function. Adding a new check (say, an IP blocklist) means editing the central function and re-testing everything. Removing a check for a particular route requires yet more conditional logic.

The Chain of Responsibility pattern lets you represent each step as a self-contained handler object. You assemble the chain at configuration time, and each handler only knows about the next one in line. The business logic handler at the end of the chain never needs to know about authentication — it simply trusts that if a request arrived, it already passed all earlier checks.

Class Diagram

Loading diagram...

The key structural points are:

  • Handler is an interface that defines the contract: every handler must be able to set a successor and handle a request.
  • AbstractHandler provides the base forwarding behavior — if a subclass does not handle the request, it calls next.handle(request) automatically.
  • Concrete handlers like AuthHandler and RateLimitHandler contain only the logic they own. They call super.handle() (or the equivalent) to pass control down the chain.
  • The client assembles the chain by calling setNext() and only holds a reference to the first handler.

Implementation

The implementations below model an HTTP middleware pipeline. Each handler inspects a simple request object and either rejects it early or passes it to the next handler. The final handler returns the actual response.

interface Request {
  token: string;
  clientId: string;
  path: string;
}

interface Response {
  status: number;
  body: string;
}

interface Handler {
  setNext(handler: Handler): Handler;
  handle(request: Request): Response;
}

abstract class AbstractHandler implements Handler {
  private next: Handler | null = null;

  setNext(handler: Handler): Handler {
    this.next = handler;
    // Return the handler so calls can be chained: a.setNext(b).setNext(c)
    return handler;
  }

  handle(request: Request): Response {
    if (this.next) {
      return this.next.handle(request);
    }
    return { status: 404, body: "No handler found" };
  }
}

class AuthHandler extends AbstractHandler {
  private validTokens = new Set(["token-abc", "token-xyz"]);

  handle(request: Request): Response {
    if (!this.validTokens.has(request.token)) {
      console.log("AuthHandler: rejected — invalid token");
      return { status: 401, body: "Unauthorized" };
    }
    console.log("AuthHandler: token valid, passing on");
    return super.handle(request);
  }
}

class RateLimitHandler extends AbstractHandler {
  private requestCounts = new Map<string, number>();
  private readonly limit = 3;

  handle(request: Request): Response {
    const count = (this.requestCounts.get(request.clientId) ?? 0) + 1;
    this.requestCounts.set(request.clientId, count);
    if (count > this.limit) {
      console.log("RateLimitHandler: rejected — quota exceeded");
      return { status: 429, body: "Too Many Requests" };
    }
    console.log(`RateLimitHandler: request ${count}/${this.limit}, passing on`);
    return super.handle(request);
  }
}

class LoggingHandler extends AbstractHandler {
  handle(request: Request): Response {
    console.log(`LoggingHandler: ${request.path}`);
    const response = super.handle(request);
    console.log(`LoggingHandler: responded ${response.status}`);
    return response;
  }
}

class BusinessHandler extends AbstractHandler {
  handle(request: Request): Response {
    console.log("BusinessHandler: processing request");
    return { status: 200, body: `Hello from ${request.path}` };
  }
}

// Assemble the chain
const auth = new AuthHandler();
const rateLimit = new RateLimitHandler();
const logging = new LoggingHandler();
const business = new BusinessHandler();

auth.setNext(rateLimit).setNext(logging).setNext(business);

// Test cases
const validRequest: Request = { token: "token-abc", clientId: "client-1", path: "/api/data" };
const badToken: Request = { token: "bad-token", clientId: "client-2", path: "/api/data" };

console.log(auth.handle(validRequest)); // { status: 200, body: "Hello from /api/data" }
console.log(auth.handle(badToken));     // { status: 401, body: "Unauthorized" }

Handler Ordering Matters

The order you assemble the chain has direct consequences. Authentication should always come before rate limiting — there is no point counting requests from clients that will be rejected anyway. Logging usually goes near the end of the guard checks so that only legitimate, counted requests are logged. Think of the chain as a series of gates: place the cheapest and most frequently triggered checks first to fail fast and avoid unnecessary work.

Real-World Examples

HTTP middleware pipelines — Express.js, ASP.NET Core, Django middleware, and Go's net/http handler chaining all implement Chain of Responsibility. Each middleware function receives the request and a next function to forward it. You can add CORS headers, parse JWT tokens, decompress request bodies, or enforce HTTPS — each as a separate, composable middleware — without any of them knowing about each other.

Support ticket escalation — A customer support system routes tickets through tiers: a chatbot handles common FAQs, a front-line agent handles product questions, a specialist handles billing disputes, and management handles legal issues. Each tier attempts to resolve the ticket and escalates only when it cannot. Adding a new tier (say, a dedicated security team) requires no changes to any existing tier — just insert a new handler into the chain.

Event bubbling in the DOM — Browser event propagation is a classic example. A click event fires on the target element and bubbles up through its ancestors. Each element in the chain can handle the event (e.g., stop propagation) or let it continue up the tree. This is why event.stopPropagation() exists — it breaks the chain at that node.

Approval workflows — Purchase order systems often require approval at increasing authority levels based on the order amount. A team lead approves orders under $1,000, a manager approves up to $10,000, a VP approves up to $100,000, and a CFO approves anything above that. The chain structure makes it trivial to add a new approval tier or change the thresholds in one place.

Pros and Cons

Advantages

Open/Closed Principle — you can add new handlers or reorder the chain without touching existing handler code. Each handler is a closed unit of logic.

Single Responsibility Principle — each handler contains exactly one concern. Authentication is not tangled with rate limiting, which is not tangled with logging.

Flexible chain composition — chains can be assembled differently per route, per tenant, or per environment at runtime. The same handlers can be reused in different orders for different pipelines.

Fail-fast behavior — handlers that reject early (authentication, rate limiting) prevent unnecessary work by later handlers, improving throughput under load.

Disadvantages

No guarantee of handling — if no handler in the chain processes the request, it falls off the end. You must decide whether to add a default terminal handler or accept that some requests go unhandled.

Debugging can be tricky — when a request is rejected, you may need to trace through several handlers to find out which one triggered and why. Logging in each handler helps, but adds verbosity.

Performance overhead — passing a request through many handlers adds function call overhead. For extremely high-throughput systems this can be measurable. Compiled middleware pipelines (like ASP.NET Core's request delegate chain) mitigate this, but hand-rolled chains in interpreted languages pay the full cost.

Chain misconfiguration — assembling the wrong order or forgetting to call the next handler are silent bugs. A rate limiter that never calls next will always drop requests without obvious error messages.

When to Use / When to Avoid

Use Chain of Responsibility when:

  • More than one object may handle a request, and the handler is not known at compile time.
  • You want to decouple the sender from the receiver — the sender should not know which specific object handles its request.
  • You need to assemble processing pipelines that vary by runtime configuration (different middleware per route, per feature flag, per tenant).
  • You have a sequence of checks where any step can terminate the process early.
  • The set of handlers will grow or change over time, and you want to add new ones without modifying existing code.

Avoid Chain of Responsibility when:

  • Every request must always be handled — the pattern's inherent "may not be handled" behavior becomes a liability.
  • The chain is always exactly one handler — you gain no flexibility and only add indirection.
  • Performance is critical and the request processing logic is simple enough to fit in a single well-structured function.
  • The order of handlers is complex and inter-dependent — use a more explicit workflow engine or state machine instead.

Decorator — structurally similar: both wrap a core operation in a chain of objects. The key difference is intent. Decorator adds behavior to an object (always forwards and always augments), while Chain of Responsibility is about deciding who handles a request (a handler may stop the chain entirely). HTTP middleware leans closer to Decorator, while approval workflows lean closer to Chain of Responsibility.

Command — often used together. A Command object encapsulates a request, and Chain of Responsibility routes it to the appropriate handler. Command handles the "what to do" while Chain of Responsibility handles the "who does it."

Composite — can be combined with Chain of Responsibility when handlers themselves form a tree. A composite handler can broadcast a request to multiple child handlers, with each child deciding whether to process or forward it.

Observer — both decouple senders from receivers, but differently. Observer notifies all interested parties simultaneously (broadcast). Chain of Responsibility routes a request to one handler at a time (sequential, exclusive handling).

Design Tip

When building HTTP middleware pipelines, prefer the framework's native chain mechanism (Express middleware, ASP.NET Core pipeline, Go's http.Handler wrapping) over rolling your own Chain of Responsibility implementation. Frameworks provide built-in error propagation, timeout handling, and context passing that are easy to get wrong from scratch. Reserve a hand-rolled chain for domain-level pipelines — such as approval workflows or request validation rules — where the framework abstractions do not apply.

On this page