LLD Dojo

Decorator

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

Start with the problem

A request pipeline needs to record every response after it is served, including its final status code. The obvious place to write that is right after routing runs.

Response handle(Request request) {
    Response response = route(request);
    accessLog.add(request.method() + " " + request.path() + " -> " + response.status());
    return response;
}

That reads fine as long as this one behavior, logging after the fact, is the only thing added around the actual routing call.

Watch where it goes

Response timing arrives next: how long the route handler took to answer. That also has to run after the handler, wrapped around the same call, and it needs its own before timestamp taken first. A third requirement asks for a header to be added to every outgoing response, still wrapped around the same call, in a specific order relative to the other two.

Each of these behaves the same way: run something before the real work, run something after it, and pass the response through, possibly changed. Writing all three inline in handle means three concerns tangled into one method, and turning any one of them off means finding and deleting its lines out of the middle of the others.

The move

Give each behavior its own wrapper, one that receives the next thing to call, calls it, and does its own work on either side.

public final class AccessLog implements Middleware {
    private final List<String> lines = new ArrayList<>();

    @Override
    public Response handle(Request request, Chain chain) {
        Response response = chain.next(request);
        lines.add(request.method() + " " + request.path() + " -> " + response.status());
        return response;
    }
}

AccessLog cannot record a status without waiting for chain.next(request) to return. That is exactly what marks it as wrapping the call rather than merely forwarding it. corpus/middleware-router's own Chain and Middleware interfaces make this the natural shape. A link receives the rest of the pipeline as a value it can call whenever it wants, before its own work, after it, or both. Wiring three of these together nests them, each one wrapping the next, the same way three decorators would wrap one base component.

static Chain wrapping(List<Middleware> middlewares, Chain terminal) {
    Chain chain = terminal;
    for (int i = middlewares.size() - 1; i >= 0; i--) {
        Middleware middleware = middlewares.get(i);
        Chain rest = chain;
        chain = request -> middleware.handle(request, rest);
    }
    return chain;
}

Adding response timing is one more Middleware, wired in wherever it needs to sit relative to AccessLog, and neither existing wrapper is touched to make room for it.

What modern Java changes here

The book's version of this pattern usually shows a Component interface, a ConcreteComponent, and a Decorator abstract class that each wrapper extends, holding a reference to the thing it wraps. In practice, a decorator whose job is "run this, then run that around it" is a function from one call shape to the same call shape, and Middleware.handle(Request, Chain) already is that function. java.io.BufferedInputStream wrapping a FileInputStream, or a Comparator built with .reversed(), are the same idea living in the standard library: something implementing the exact interface it wraps, adding one behavior around a delegated call.

The one thing this shape adds beyond a textbook decorator: chain.next returns a Response rather than nothing, so a wrapper can act on the way out as well as the way in. AccessLog only reads the response. A wrapper that rewrote it would build a new Response, not mutate the one that came back, since Response is a record and has no setters to mutate in the first place.

When naming it is wrong

One behavior, wrapped once, with no second wrapper ever planned, is a method that does two things in sequence. Writing AccessLog as its own class, to wrap a pipeline that will only ever have this one cross-cutting concern, buys an extra file and an extra layer of indirection. Nothing justifies that second layer yet.

The threshold: reach for a wrapper once two or more before-or-after behaviors need to compose in an order that might change. The other trigger is a behavior that needs to be added or removed without touching the others. A single fixed behavior, applied once, with nothing else stacking on top of it, is over-application. The Standard's D3 dimension marks that the same way it marks any speculative seam with one real implementation.

Where this lives in the app

Syllabus item B7 carries this shape too, alongside Chain of Responsibility, on the same corpus/middleware-router. AccessLog and a response-timing wrapper both fit as links that act on the way out, not only the way in. Middleware.handle's own javadoc names the distinction from a plain chain directly.

All reference pages