LLD Dojo

Chain of Responsibility

Core. Expect to meet this one, and expect to be asked for it by name.

Start with the problem

A request router needs to check a few things before it matches a route: is the caller allowed near this path at all, and should this request be recorded. The obvious place to put both checks is right where a request comes in.

Response handle(Request request) {
    if (request.path().startsWith("/admin") && !isAuthorized(request)) {
        return new Response(403, "forbidden");
    }
    Response response = route(request);
    accessLog.add(request.path() + " -> " + response.status());
    return response;
}

For two rules, this reads top to bottom and does what it says.

Watch where it goes

A third rule arrives: rate limiting for a specific path prefix. A fourth: a header check for a different prefix. Each one is its own if, unrelated to the ones already there, and handle keeps absorbing them regardless of whether the rule belongs to security, to logging, or to a completely different team.

The rules themselves are not the problem. What is expensive is different. Legal wants rejected requests kept out of the access log, and that change is owned by one team. Making it means editing a method three other teams also depend on, and reading past their checks first to find where the new one goes.

The move

Turn each rule into its own link, and chain the links together so each one decides whether to act and then passes the request on.

@FunctionalInterface
public interface Chain {
    Response next(Request request);
}

@FunctionalInterface
public interface Middleware {
    Response handle(Request request, Chain chain);
}

public final class AdminGate implements Middleware {
    private final String validToken;

    public AdminGate(String validToken) {
        this.validToken = validToken;
    }

    @Override
    public Response handle(Request request, Chain chain) {
        if (request.path().startsWith("/admin") && !validToken.equals(request.method())) {
            return new Response(403, "forbidden");
        }
        return chain.next(request);
    }
}

This is corpus/middleware-router's shape, and AdminGate is drawn from its 03-short-circuit-audit curveball. A gate that only ever decides "stop here" or "pass it on" fits one link in the chain, wired in with a single call. The security team's rule never touches the routing code at all. Against that same curveball, this design absorbs the requirement for a reference_diff of 0: no file in reference/src changes.

What modern Java changes here

The pipeline itself is nothing more than a List<Middleware> folded right to left into one Chain. Each link is a functional interface. Most links in practice end up as lambdas or small final classes, not a class hierarchy under an abstract Handler. Ordering matters here in a way the original catalogue does not stress. The gate has to run before anything that would log a rejected request, so registration order is part of the design, not an incidental detail.

Watch the boundary with a plain if-chain, too. A pure Chain of Responsibility link only ever decides to handle a request or pass it along. Once a link needs to act on what comes back from the rest of the chain, the shape has moved into wrapping a call, not merely forwarding it. See decorator.md for that half of the same corpus.

When naming it is wrong

A chain answers one question well: "should this also run for every request coming through." It answers a different kind of question badly. Once a rule depends on which route or method a request is, the single ordered list has nowhere to keep that decision. Every link ends up re-deriving routing information the router already had. On this corpus's own measurement, making one link's behavior depend on the HTTP method costs 47 lines to retrofit. That is the most expensive change measured anywhere in this corpus, against a design built around a router from the start.

The threshold: if the requirement reads "do this to everything passing through," it is another link, and the chain absorbs it for the price of one file. If it reads "decide which steps apply, or in what order, based on something about this particular request," reach for a router instead. Two links wired for one caller, with no cross-cutting rule in sight, is not this pattern. It is a conditional wearing an interface.

Where this lives in the app

Syllabus item B7 measures both directions on corpus/middleware-router. A security gate and an access-log rule both cost nothing to add. A per-route split costs 47 lines, and when-not.md states the rule for telling the two apart before you build either.

All reference pages