One function handles multiple orthogonal concerns sequentially. Adding a new concern (CORS, body parsing, request tracing) means editing the same function; the function changes for entirely unrelated reasons; reviewing it requires holding the full concern set in working memory.
Each concern is a small handler class that decides whether to handle the request or forward it down the chain. The chain composition is explicit at the construction site; adding a new concern is a new handler class + an additional link in the chain.
Before the pattern
function handleRequest(req, res) {if (!req.headers.authorization) {res.status = 401;res.body = 'unauthorized';return;}if (rateLimiter.exceeded(req.ip)) {res.status = 429;res.body = 'too many requests';return;}logger.info(`${req.method} ${req.path}`);if (req.path === '/api/users') {res.status = 200;res.body = listUsers();return;}res.status = 404;}// One function changes for auth reasons, rate-limit reasons, logging reasons,// routing reasons — every concern is a Divergent Change pressure on the same body.
After the pattern
class Handler {constructor(next) {this.next = next;}handle(req, res) {if (this.next) this.next.handle(req, res);}}class AuthHandler extends Handler {handle(req, res) {if (!req.headers.authorization) {res.status = 401;res.body = 'unauthorized';return;}super.handle(req, res);}}class RateLimitHandler extends Handler {handle(req, res) {if (rateLimiter.exceeded(req.ip)) {res.status = 429;res.body = 'too many requests';return;}super.handle(req, res);}}class LoggingHandler extends Handler {handle(req, res) {logger.info(`${req.method} ${req.path}`);super.handle(req, res);}}class RoutingHandler extends Handler {handle(req, res) {if (req.path === '/api/users') {res.status = 200;res.body = listUsers();return;}res.status = 404;}}const chain = new AuthHandler(new RateLimitHandler(new LoggingHandler(new RoutingHandler(null))));chain.handle(req, res);
A single handleRequest function changing for auth reasons, rate-limit reasons, and logging reasons in the same week is Divergent Change at its worst — the function's git history is incoherent, code review becomes change-by-change instead of body-by-body, and conflicts during merges multiply.
Chain composition is dynamic — the order matters (logging before or after auth changes what gets logged for unauthorized requests), and the ordering lives at the construction site, not in the handler classes themselves. Stack traces span every handler in the chain; debugging requires understanding the wiring.
Each handler is small, single-responsibility, unit-testable; the chain is composable; new concerns ship as new handlers without touching existing ones. Tests for each handler cover its own behaviour; chain-level tests cover ordering interactions explicitly.
Handlers that look at chain state (which handler ran before me, will the next handler bail out, did the previous handler short-circuit) reintroduce coupling the pattern was supposed to eliminate. Chain ordering becomes structural; replacing one handler requires understanding the full chain's behaviour. The chain becomes a thinly-disguised long function spread across files.