Skip to article
ALGORITHMICSPatterns
Patterns5 min read

Chain of Responsibility

Pass the request along until somebody claims it — and the case nobody handles.


An incoming HTTP request has to be authenticated, rate-limited, checked against a cache, and finally routed. Any of those steps might end it early.

What people write first

function handle(request: Request): Response {
if (!request.user) return unauthorized();
if (overRateLimit(request)) return tooManyRequests();
const cached = cache.get(request);
if (cached) return cached;
return route(request);
}

Honestly? For four fixed steps this is good code, and you should not replace it.

It becomes a problem when the set of steps varies — when some routes need CORS and others need a tenant lookup, when a plugin wants to insert a step, or when the order needs to be configurable. Then this function grows flags, and the flags interact.

The pattern

Each step is an object that either handles the request or passes it on.

interface Handler {
setNext(next: Handler): Handler;
handle(request: Request): Response | null;
}
abstract class BaseHandler implements Handler {
private next?: Handler;
setNext(next: Handler): Handler {
this.next = next;
return next; // returns next, so chains read in order
}
handle(request: Request): Response | null {
return this.next?.handle(request) ?? null;
}
}
class RateLimit extends BaseHandler {
handle(request: Request) {
if (overRateLimit(request)) return tooManyRequests();
return super.handle(request); // ← not mine; pass it on
}
}

Watch a request walk the chain:

1 / 3

request: cached

auth not mine
rateLimit
cache
router

auth looks at the request, decides it is not its concern, and passes it along. It does not know who is next; it only knows there is a next.

Assembly is separate from the handlers, which is the point:

const chain = new Auth();
chain.setNext(new RateLimit()).setNext(new Cache()).setNext(new Router());

The middleware variant

Modern frameworks use a version where each step can act after the rest of the chain as well as before:

type Middleware = (req: Request, next: () => Promise<Response>) => Promise<Response>;
const timing: Middleware = async (req, next) => {
const started = Date.now();
const response = await next(); // ← everything downstream
metrics.timing('request', Date.now() - started);
return response;
};

That single await next() is what lets one handler wrap the whole remainder — logging, timing, transactions, error boundaries. It is strictly more powerful than the classic version, which can only decide whether to continue.

Express, Koa, ASP.NET Core and Django all use this shape. If you are writing a chain today, write this one.

Where it already is

Servlet filters, DOM event bubbling, exception handlers unwinding a stack, logging frameworks passing a record up through appenders, and Unix signal handlers. All the same shape: an ordered list, each member free to stop the walk.