Chain of Responsibility
Chain of Responsibility passes a request along a series of handlers, each of which decides to handle it or pass it on. Use it for middleware pipelines, escalation flows, parsing, request validation — anywhere a request needs to be processed by one of many alternatives without coupling sender to handler.
Handler interface, chain, escalation
EXAMPLE
// 1) The shape — handler interface + next pointer
interface Handler<TReq, TRes> {
setNext(h: Handler<TReq, TRes>): Handler<TReq, TRes>;
handle(req: TReq): TRes | null;
}
abstract class BaseHandler<TReq, TRes> implements Handler<TReq, TRes> {
private next: Handler<TReq, TRes> | null = null;
setNext(h: Handler<TReq, TRes>) {
this.next = h;
return h;
}
handle(req: TReq): TRes | null {
return this.next?.handle(req) ?? null;
}
}
// 2) Real example — purchase approval
interface PurchaseRequest { amountCents: number; requester: string }
interface Decision { approved: boolean; approver: string }
class TeamLead extends BaseHandler<PurchaseRequest, Decision> {
handle(req: PurchaseRequest): Decision | null {
if (req.amountCents <= 50_000) return { approved: true, approver: 'team-lead' };
return super.handle(req);
}
}
class Manager extends BaseHandler<PurchaseRequest, Decision> {
handle(req: PurchaseRequest): Decision | null {
if (req.amountCents <= 500_000) return { approved: true, approver: 'manager' };
return super.handle(req);
}
}
class Director extends BaseHandler<PurchaseRequest, Decision> {
handle(req: PurchaseRequest): Decision | null {
if (req.amountCents <= 5_000_000) return { approved: true, approver: 'director' };
return super.handle(req);
}
}
class Board extends BaseHandler<PurchaseRequest, Decision> {
handle(req: PurchaseRequest): Decision | null {
return { approved: false, approver: 'board' }; // board defers to a vote, not auto-approve
}
}
// Build the chain
const chain = new TeamLead();
chain.setNext(new Manager()).setNext(new Director()).setNext(new Board());
chain.handle({ amountCents: 30_000, requester: 'Mara' }); // { approved: true, approver: 'team-lead' }
chain.handle({ amountCents: 2_000_000, requester: 'Mara' }); // { approved: true, approver: 'director' }
// 3) Middleware pipeline — Express style
type Next = () => Promise<void>;
type Middleware = (ctx: any, next: Next) => Promise<void>;
class Pipeline {
private mws: Middleware[] = [];
use(mw: Middleware) { this.mws.push(mw); return this; }
async run(ctx: any) {
let i = -1;
const dispatch = async (idx: number) => {
if (idx <= i) throw new Error('next() called twice');
i = idx;
const mw = this.mws[idx];
if (mw) await mw(ctx, () => dispatch(idx + 1));
};
await dispatch(0);
}
}
const pipe = new Pipeline()
.use(async (ctx, next) => { ctx.start = Date.now(); await next(); console.log('took', Date.now() - ctx.start, 'ms'); })
.use(async (ctx, next) => { if (!ctx.user) return ctx.res = { status: 401 }; await next(); })
.use(async (ctx) => { ctx.res = { status: 200, body: `Hi ${ctx.user.name}` }; });
await pipe.run({ user: { name: 'Mara' } });
// 4) Validation pipeline
type Validator<T> = (value: T) => string | null;
function chainValidators<T>(...vs: Validator<T>[]): Validator<T> {
return (value) => {
for (const v of vs) {
const err = v(value);
if (err) return err;
}
return null;
};
}
const validate = chainValidators<string>(
(s) => s.length === 0 ? 'required' : null,
(s) => s.length > 100 ? 'too long' : null,
(s) => /\\s/.test(s) ? 'no whitespace' : null,
);
validate(''); // 'required'
validate('hello world'); // 'no whitespace'
validate('mara'); // null (valid)
// 5) Decoupling sender from handler
// The CALLER doesn't know:
// • Which handler will respond
// • How many handlers exist in the chain
// • What order they're in
// Adding a new handler = one new class + one .setNext() call.
// 6) Optional vs Mandatory handling
// • If at least ONE handler must respond — make the last handler a catch-all
// • If handling is optional — return null from the last handler; caller handles undefined
// 7) Stop on first handler vs run all
// • Classic CoR: stop at first that handles
// • Variant 'broadcast': every handler runs (pre/post processing); used in event systems and DOM events
// • Variant 'until consumed': run until one signals 'stop' (e.g. preventDefault())
// 8) Dynamic chain construction
function buildChain(level: 'low' | 'med' | 'high'): Handler<PurchaseRequest, Decision> {
const tl = new TeamLead();
if (level === 'low') return tl;
const m = new Manager(); tl.setNext(m);
if (level === 'med') return tl;
const d = new Director(); m.setNext(d).setNext(new Board());
return tl;
}
// 9) Tree of Responsibility — handlers fan out, not just sequentially
// Useful for parsing, routing, decision trees. Each handler can send to multiple children.
// 10) Real-world examples
// • Express / Koa / Connect middleware pipelines
// • DOM event capture/bubble phases
// • Logging frameworks (filters before / after handlers)
// • Servlet filters in Java
// • Authorization checks (user → group → org → admin)
// • LSP language server middleware
// • Parsing pipelines (lex → parse → semantic → codegen)
// • CI pipelines (each stage hands off to the next)
// 11) Trade-offs
// + Decouples sender from handler
// + Easy to add / remove / reorder handlers
// + Single Responsibility per handler
// + Naturally handles 'try each' fallback logic
//
// − Tracing through a long chain is harder than a switch statement
// − No guarantee request will be handled (use a default catch-all)
// − Adding global concerns (logging) requires wrapping or a special handler
// 12) Composition with other patterns
// • Strategy — handler can delegate to a strategy for the actual work
// • Command — request can be a Command object that the handler executes
// • Observer — handler can publish events for monitoring
// • Decorator — wrap a chain with cross-cutting concerns (logging, timing)
// 13) Common bugs
// • Forgetting to call super.handle() / next() — chain stops silently
// • Calling next() twice in middleware — double-execution; throw to catch
// • Circular chain — A.next = B, B.next = A — infinite loop; validate at construction
// • No catch-all handler — unhandled requests return null without explanation
// • Mutating the request object in one handler that breaks downstream handlers — make requests immutable
// • Async + missing await — next handler runs before previous completed
// • Caching the chain instance across threads (Java) — state leakage; build per-request
// • Treating the chain as a 'middleware soup' without a clear contract — document handler responsibilities
Why it matters
Chain of Responsibility decouples request from handler — useful for middleware pipelines, escalation flows, validation chains, and any “try each in order” logic. Pick between “stop on first handled” (classic) and “run all” (event systems), make requests immutable, and always have a catch-all so unhandled requests can’t silently slip through.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
function pipeline(...handlers) {
return req => handlers.reduce((acc, h) => h(acc), req);
}
const handle = pipeline(parse, authenticate, route);
Try it Yourself »
Discussion
Loading…