cv.cm

Coding · Design patterns

Chain of Responsibility pattern explained with examples

5 min read

Short answer

Chain of Responsibility is a behavioral design pattern that passes a request along a chain of handler objects. Each handler either processes the request or forwards it to the next handler in the chain. The sender does not know which handler will deal with the request, so you can add, remove or reorder handlers without changing the sender. Web middleware and DOM event bubbling are well-known examples.

Intent and the problem it solves

Intent: avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it. The pattern is one of the behavioral patterns in the Gang of Four (GoF) book.

Consider an online shop's support system. A refund request below 50 can be approved by a support agent, up to 500 by a team lead, and anything larger needs a manager. A first version often puts all of this into one function with a long if/else chain. Every new rule makes that function longer, and the order of checks is hard-coded.

With Chain of Responsibility, each level becomes its own handler with one rule. The handlers are linked together, and the request enters at the first one. Adding a new approval level means writing one small class and inserting it into the chain.

Structure and participants

RoleResponsibility
HandlerDeclares the method for handling requests and keeps a reference to the next handler
Concrete handlerHandles the requests it is responsible for; otherwise forwards the request to the next handler
ClientBuilds the chain (or receives it) and sends the request to the first handler

There are two common variations. In the classic form, the first handler that can process the request stops the chain. In the pipeline form (used by most middleware), every handler may do some work and then decide whether to call the next one.

TypeScript implementation

An abstract base class stores the next handler and provides the default forwarding behavior. Each concrete handler overrides handle and calls super.handle when it cannot decide.

type Refund = { amount: number };
abstract class Approver {
  private next?: Approver;
  setNext(next: Approver): Approver {
    this.next = next;
    return next; // allows a.setNext(b).setNext(c)
  }
  handle(req: Refund): string {
    return this.next ? this.next.handle(req) : "rejected: no one can approve " + req.amount;
  }
}
class Agent extends Approver {
  handle(req: Refund): string {
    return req.amount < 50 ? "agent approved " + req.amount : super.handle(req);
  }
}
class TeamLead extends Approver {
  handle(req: Refund): string {
    return req.amount <= 500 ? "team lead approved " + req.amount : super.handle(req);
  }
}
class Manager extends Approver {
  handle(req: Refund): string {
    return req.amount <= 5000 ? "manager approved " + req.amount : super.handle(req);
  }
}
const chain = new Agent();
chain.setNext(new TeamLead()).setNext(new Manager());
for (const amount of [20, 300, 2000, 9000]) console.log(chain.handle({ amount }));
// agent approved 20
// team lead approved 300
// manager approved 2000
// rejected: no one can approve 9000

The middleware form with plain functions

In TypeScript you do not need classes. A list of functions, each receiving a next callback, forms a chain. This is how Express and Koa middleware work.

type Req = { user?: string; path: string };
type Middleware = (req: Req, next: () => string) => string;
const auth: Middleware = (req, next) => (req.user ? next() : "401 Unauthorized");
const log: Middleware = (req, next) => {
  console.log("request", req.path);
  return next();
};
const route = (req: Req) => "200 OK " + req.path;
function run(chain: Middleware[], req: Req): string {
  const step = (i: number): string =>
    i < chain.length ? chain[i](req, () => step(i + 1)) : route(req);
  return step(0);
}
console.log(run([log, auth], { path: "/admin" }));
console.log(run([log, auth], { user: "ana", path: "/admin" }));
// request /admin
// 401 Unauthorized
// request /admin
// 200 OK /admin

When to use it and when not to

  • Use it when several objects may handle a request and the right one is decided at runtime.
  • Use it when the set or order of handlers should be configurable, for example validation steps, approval levels or HTTP middleware.
  • Use it when you want each rule in its own small unit that can be tested alone.
  • Do not use it for two or three fixed checks that never change; a simple if statement is clearer.
  • Be aware that long chains are harder to debug, because it is not obvious from the call site which handler finally acted. Logging inside handlers helps.
  • Chains are sensitive to order. Putting authentication after the route handler, for example, defeats its purpose.

Real-world uses

  • Express and Koa middleware: each function receives the request and a next function and decides whether to continue.
  • DOM event propagation: an event bubbles from the target element up through its ancestors, and any listener can stop it with event.stopPropagation().
  • Java servlet filters form a filter chain in which each filter calls chain.doFilter to pass the request on.
  • Logging frameworks pass log records from a logger to its parent loggers and their handlers.

Related patterns

PatternHow it differs from Chain of Responsibility
DecoratorDecorators also form a chain of wrappers, but every decorator always runs and forwards; a handler may stop the chain.
CommandA request in the chain is often a command object; Command is about packaging the request, the chain is about finding who handles it.
CompositeA parent in a Composite tree is a natural next handler, which is how GUI events bubble up the component tree.
MediatorMediator routes messages through one central object that knows the receivers; a chain passes the request from one handler to the next.

FAQ

Does every handler in the chain have to process the request?

No. In the classic form the first handler that can process the request stops the chain. In the middleware form each handler may do some work and then call the next one, so several handlers can act on one request.

Is Express middleware an example of Chain of Responsibility?

Yes. Each middleware function receives the request and a next callback, and it either ends the response or calls next to pass control to the following middleware.

What happens if no handler handles the request?

That depends on your design. Good practice is to add a default at the end of the chain, such as returning an error, throwing, or logging, so requests are never lost silently.

How is it different from a list of if/else statements?

The logic is the same, but each condition becomes a separate object or function. That makes the chain configurable at runtime and lets you add or reorder steps without editing one large function.

All coding tutorials