The idea: hand the request to the first handler
In a chain of responsibility the sender does not pick who deals with a request. It gives the request to the first handler in a chain. Each handler looks at it and does one of two things: it handles it, or it passes it on to the next handler. The sender never learns which handler answered.
The canvas is an Express-style web app. The chain is the order of the app.use() calls. Each handler is a middleware function (req, res, next): it answers with res.status(…).send(…), or it calls next(). Under each box, saw counts the requests that reached it and handled counts the ones it answered.
Handle it or pass it on
Each handler checks one thing and knows nothing about the others:
loggerwrites a log line and always callsnext(). It never answers.rateLimit({max: 3})counts requests in the window. The 4th gets429 Too Many Requests(Demo 4: rate limit, the 4th request gets 429).authanswers401 Unauthorizedwhen there is no token, otherwise setsreq.userand callsnext()(Demo 2: auth stops it early).express.staticserves files under/static/; anything else is not its job.apiRouteranswersGET /api/orders(Demo 1: passed all the way to the router).
When a handler answers, it does not call next(), so the chain stops there. The faded boxes on the canvas are the handlers that never saw the request. In the classic form from the Design Patterns book, each handler object holds a link to the next one:
abstract class Handler {
private Handler next;
Handler setNext(Handler h) { next = h; return h; }
Response handle(Request r) {
return next != null ? next.handle(r) : null; // pass it on
}
}
class Auth extends Handler {
Response handle(Request r) {
if (r.token == null) return new Response(401); // handle it
return super.handle(r);
}
}
logger.setNext(rateLimit).setNext(auth).setNext(staticFiles).setNext(apiRouter);
The sender only holds logger. You can add, remove or reorder handlers without touching the sender or the other handlers.
Falling off the end
Nothing guarantees that some handler will take the request. GET /api/users passes every handler, because no one has a rule for it (Demo 3: nobody handles it). Express puts a final handler at the end that answers 404 Not Found; on the canvas that is notFound. Without such a catch-all the request would simply get no answer, or in the classic form, handle() returns null.
The order of the chain matters
The same handlers in another order give other answers. With auth before static, the public logo needs a login: GET /static/logo.png without a token gets 401. Move express.static to the front and the logo is served with 200, and it no longer counts against the rate limit, because rateLimit never sees it (Demo 5: order matters, static before auth). This is why Express apps register express.static early and the 404 handler last.
Three forms of the chain
| Pure chain | Middleware pipeline | DOM event bubbling | |
|---|---|---|---|
| Who acts | the first handler that can | many handlers do a little, one answers | every listener on the path |
| Stop the chain | handle it | send a response, skip next() | event.stopPropagation() |
| Chain order | set up by code (setNext) | app.use() order | the element tree, child to parent |
| Examples | expense approval, help desk escalation, exception handlers | Express, Koa, ASP.NET Core, Java Servlet filters | clicks, key presses in the browser |
Chain of responsibility vs decorator
Both build a line of objects that each hold a link to the next. A decorator always passes the call on and adds something around it; the real object at the end always runs. A chain handler may stop the request, and there may be no real object at the end at all. Middleware sits between the two: a logger acts like a decorator, auth acts like a chain handler. See Decorator and Proxy for the wrapper side, Observer and Pub/Sub for sending one event to all listeners instead of one, and the Command pattern for turning a request into an object you can pass along.
What the page leaves out
Async handlers and error-handling middleware (Express's four-argument (err, req, res, next)); handlers that change the response on its way back (Koa's onion model); the rate-limit window expiring and per-IP counters; event capturing (the phase before bubbling) and stopImmediatePropagation(); and the cost of a long chain where most handlers just pass the request on.