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.

A client sends GET /api/orders without a token. logger and rateLimit each call next(); auth sees no token, handles the request and sends 401 Unauthorized back to the client. static and apiRouter are faded: they never see the request
The request goes along the chain until one handler handles it; the handlers after that one never run.

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:

  • logger writes a log line and always calls next(). It never answers.
  • rateLimit({max: 3}) counts requests in the window. The 4th gets 429 Too Many Requests (Demo 4: rate limit, the 4th request gets 429).
  • auth answers 401 Unauthorized when there is no token, otherwise sets req.user and calls next() (Demo 2: auth stops it early).
  • express.static serves files under /static/; anything else is not its job.
  • apiRouter answers GET /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

Three chains side by side. Pure chain: a 3,000 dollar expense passes TeamLead with a 1,000 dollar limit and is approved by Manager; Director is never asked. Middleware: logger and auth both act and call next, apiRouter answers 200. DOM event bubbling: a click runs the button's listener, then the form's listener, which calls stopPropagation, so body never sees the event
In a pure chain exactly one handler acts; in a pipeline many act and one answers; DOM events bubble from the child to its parents.
Pure chainMiddleware pipelineDOM event bubbling
Who actsthe first handler that canmany handlers do a little, one answersevery listener on the path
Stop the chainhandle itsend a response, skip next()event.stopPropagation()
Chain orderset up by code (setNext)app.use() orderthe element tree, child to parent
Examplesexpense approval, help desk escalation, exception handlersExpress, Koa, ASP.NET Core, Java Servlet filtersclicks, 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.

On the mind map

Open the Software Design mind mapSoftware DesignPrinciples: keep each change localPrinciplesDesign patterns: named fixes for common designsDesign patternsArchitecture: the shape of a whole appArchitectureEnterprise app patterns: logic, SQL, locks, money (PoEAA)Enterprise app patternsGit storage: content named by its hashGit storageGit branches & merging: pointers to commits, then mergeGit branches & mergingTesting: check code in isolationTestingStrategy and FactoryStrategy and Factorypick behaviour by name, swap at runtimeState patternState patternstate objects replace a big switchCommand and undoCommand and undoeach action is an object with undo()Decorator, Proxy, AdapterDecorator, Proxy, Adapterwrappers that share one interfaceObserver vs pub-subObserver vs pub-subobserver calls directly; pub-sub via brokerBuilder and SingletonBuilder and SingletonBuild step by step vs one shared instanceComposite and VisitorComposite and VisitorTreat a tree uniformly; add ops outside itChain of responsibilityEach handler acts or passes the request onOpen the Software Design mind mapSoftware Designnamed fixes for common designsDesign patternsStrategy and FactoryStrategy and Factorypick behaviour by name, swap at runtimeState patternState patternstate objects replace a big switchCommand and undoCommand and undoeach action is an object with undo()Decorator, Proxy, AdapterDecorator, Proxy, Adapterwrappers that share one interfaceObserver vs pub-subObserver vs pub-subobserver calls directly; pub-sub via brokerBuilder and SingletonBuilder and SingletonBuild step by step vs one shared instanceComposite and VisitorComposite and VisitorTreat a tree uniformly; add ops outside itChain of responsibilityEach handler acts or passes the request on
This page on the Software Design mind map · 28 pages in 7 branches · open it to see the whole map and test yourself in recall mode.