Behavioral Pattern Pattern 13 of 23

Chain of Responsibility

Think of an expense approval process at a company. A $50 request only needs a manager's sign-off. A $5,000 request needs a director. A $50,000 request needs a VP. The person submitting the request doesn't route it themselves — they just hand it to their manager, and it climbs the chain until someone with enough authority handles it. The Chain of Responsibility pattern is that exact idea in code: a request travels along a chain of handler objects until one of them decides it can handle it, and the sender never needs to know which one that will be.

⚠️ The Problem

Imagine building that expense approval system directly: a single function with a wall of if/else checks — if (amount <= 1000) { manager.approve(); } else if (amount <= 10000) { director.approve(); } else { vp.approve(); }. Every time the company adds a new approval tier, or changes a threshold, or inserts a new role between two existing ones, you have to go back and edit this one function. The sender of the request is also tightly coupled to every possible approver, even though it only ever needs one of them to actually act.

✅ The Solution

Give each handler (Manager, Director, VP) a reference to the "next" handler in the chain, and a consistent method like handle(request). Each handler checks whether it can approve the request; if it can, it does and stops the chain; if it can't, it forwards the request to the next handler by calling next.handle(request). The submitter just calls chain.handle(request) on the first handler — it never decides who ultimately approves it, and adding a new tier means creating one more handler and linking it in, with zero changes to the handlers that already exist.

Example 1 — an expense approval chain

Primary Example — JavaScript
class Approver {
  constructor(name, limit) {
    this.name = name;
    this.limit = limit;
    this.next = null;
  }

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

  handle(request) {
    if (request.amount <= this.limit) {
      console.log(`${this.name} approved $${request.amount} for "${request.reason}"`);
      return;
    }
    if (this.next) {
      this.next.handle(request);
    } else {
      console.log(`No one could approve $${request.amount} — request rejected.`);
    }
  }
}

const manager  = new Approver("Manager", 1000);
const director = new Approver("Director", 10000);
const vp       = new Approver("VP", 100000);

manager.setNext(director).setNext(vp);

manager.handle({ amount: 500,   reason: "Team lunch" });     // Manager approves
manager.handle({ amount: 7500,  reason: "Conference travel" }); // Director approves
manager.handle({ amount: 45000, reason: "New server rack" });   // VP approves

The code that submits a request only ever talks to manager, the first link in the chain. It has no idea whether a Director or VP even exists, and if the company later inserts a "Senior Director" tier between Director and VP, only one line — the wiring — needs to change.

Example 2 — a secondary context: HTTP middleware

Secondary Example — C#
public abstract class Middleware
{
    protected Middleware? Next;

    public Middleware SetNext(Middleware next)
    {
        Next = next;
        return next;
    }

    public abstract void Handle(HttpRequest request);
}

public class AuthMiddleware : Middleware
{
    public override void Handle(HttpRequest request)
    {
        if (!request.HasValidToken)
        {
            Console.WriteLine("401 Unauthorized — request stopped.");
            return;
        }
        Console.WriteLine("Auth passed.");
        Next?.Handle(request);
    }
}

public class LoggingMiddleware : Middleware
{
    public override void Handle(HttpRequest request)
    {
        Console.WriteLine($"Logging request to {request.Path}");
        Next?.Handle(request);
    }
}

public class RateLimitMiddleware : Middleware
{
    public override void Handle(HttpRequest request)
    {
        if (request.RequestsThisMinute > 100)
        {
            Console.WriteLine("429 Too Many Requests — request stopped.");
            return;
        }
        Console.WriteLine("Rate limit OK — handing off to route handler.");
        Next?.Handle(request);
    }
}

var auth    = new AuthMiddleware();
var logging = new LoggingMiddleware();
var limiter = new RateLimitMiddleware();

auth.SetNext(logging).SetNext(limiter);
auth.Handle(new HttpRequest("/api/orders", hasValidToken: true, requestsThisMinute: 12));

This is exactly how real web frameworks structure middleware pipelines — each middleware decides whether to short-circuit the chain (stop it, like a failed auth check) or pass control to the next link. The route handler at the very end never has to re-check auth, logging, or rate limits itself.

👍 When To Use It

  • More than one object might handle a request, and which one handles it should be decided at runtime rather than hardcoded by the sender.
  • You want to issue a request without specifying the receiver explicitly — the sender only needs to know about the first handler in the chain.
  • The set of handlers, or their order, needs to change independently of the code that issues requests — new handlers can be inserted or removed without touching senders.

👎 When NOT To Use It

  • When exactly one object should always handle every request — a direct call is simpler and doesn't need a chain to express that.
  • When it's important to guarantee a request is actually handled — a naive chain can let a request silently fall off the end if no handler claims it, which is easy to overlook.
  • When the chain gets long enough that debugging "which handler actually processed this" becomes hard — long chains trade a single if/else block for a different kind of hidden control flow, and that tradeoff isn't always a win.

❓ FAQ

How is this different from Decorator?

Structurally, both wrap objects in a linked sequence. But Decorator's whole point is that every layer in the chain runs and adds behavior — the request/response always passes through all of them. Chain of Responsibility is about choosing which single handler (or subset) actually processes the request, and any handler can stop the chain early once it's satisfied.

Can more than one handler process the same request?

Yes — nothing forces a handler to stop the chain after acting. Middleware pipelines are a good example: logging middleware can act (log the request) and still call next() to let the rest of the chain run too. Whether the chain stops after the first match or keeps going is a design choice you make per chain.

What happens if no handler in the chain can handle the request?

By default, nothing — the request falls off the end silently, which is a common bug source. Most real implementations add an explicit fallback: either the last handler in the chain is a catch-all that always handles the request, or the chain itself throws/logs an error when it reaches the end with nothing having claimed the request.

🙋 There Are No Dumb Questions

Q: Isn't this just a linked list of if-statements?
A: Structurally, yes — that's a fair way to think about it. The value isn't the linked-list mechanics, it's that each handler is a separate, independently testable object that only knows about "can I handle this, and who's next," instead of one giant function that has to know about every case at once.

✅ Quick Check

In Chain of Responsibility, what does the object that issues a request need to know?


Next up: what if you need to treat an action itself as an object — something you can store, queue, log, or undo, instead of just calling a function and having it happen immediately? That's the Command pattern.

← Back to
Next →