Intent

Pass a request along a chain of handlers. Each handler decides either to handle the request and stop, or to forward it to the next handler in the chain. The sender doesn't know which handler will end up handling it.

The Problem

You're handling an incoming HTTP request. Before the actual route handler runs, several things must happen:

  1. Authenticate the user (return 401 if no valid token).
  2. Authorize them for this specific resource (return 403 if denied).
  3. Validate the request body (return 422 if it's malformed).
  4. Apply rate limiting (return 429 if the user is over their quota).
  5. Finally, run the actual handler that does the work.

The first version stuffs all of it into the controller method:

PHP
public function update(Request $request): Response
{
    if (!$user = $this->auth->authenticate($request)) return Response::unauthorized();
    if (!$this->permissions->canEdit($user, $request->id)) return Response::forbidden();
    if ($errors = $this->validator->validate($request->body)) return Response::unprocessable($errors);
    if ($this->rateLimiter->isOverLimit($user)) return Response::tooMany();

    return $this->doTheWork($request);
}

Every controller method repeats this dance. Adding "log every request" or "trace every request with the user's locale" means editing every controller. Some endpoints need authorization but not rate limiting; others need rate limiting but skip auth. The combinations multiply, the controllers swell, and the cross-cutting concerns leak into every single endpoint.

The Solution

Chain of Responsibility says: each concern is its own handler with one method (handle(request)). Handlers are linked into a chain. A handler either short-circuits and returns a response — or forwards the request to the next handler in line.

PHP
abstract class Middleware
{
    private ?Middleware $next = null;

    public function setNext(Middleware $next): Middleware
    {
        $this->next = $next;
        return $next;   // for fluent chaining
    }

    public function handle(Request $req): Response
    {
        return $this->next?->handle($req) ?? $this->fallback($req);
    }

    protected function fallback(Request $req): Response
    {
        return Response::notFound();   // default tail of the chain
    }
}

Each concrete middleware overrides handle() to either short-circuit (return early with 401/403/422/429) or call parent::handle($req) to pass control to the next handler. The chain is assembled at boot:

PHP
$pipeline = new AuthMiddleware();
$pipeline
    ->setNext(new AuthorizationMiddleware())
    ->setNext(new ValidationMiddleware())
    ->setNext(new RateLimitMiddleware())
    ->setNext(new RouteHandler());

Adding a fifth concern is a new class plus a setNext() call. Controllers stop knowing about cross-cutting concerns entirely.

Real-World Analogy

Customer support escalation. You call about a billing issue. Tier-1 picks up. If they can solve it ("I see the duplicate charge — I'll refund it"), the call ends there. If they can't ("you'll need engineering for that"), they pass it to Tier-2. Tier-2 either solves it or escalates to Tier-3. Each tier knows only two things: can I handle this? and who's next if I can't?

Nobody on Tier-1 needs to know the entire org chart. The chain is set up by the company; agents just check their own competence and forward when stuck. Adding a Tier-4 next quarter doesn't require retraining everyone — it's just one more link added to the end.

Structure

Chain of Responsibility pattern: a Middleware base class with a next reference, and concrete handlers AuthMiddleware, ValidationMiddleware, RateLimitMiddleware that each either handle the request or forward it.
Chain of Responsibility: each handler decides whether to act or to forward.

Three roles you'll see in every Chain of Responsibility implementation:

  • Handler — the abstract base (or interface) declaring handle(request) and a way to set the next handler. Often provides a default forwarding behaviour.
  • Concrete Handler — a specific link in the chain. Decides whether the request is its responsibility. If yes, handles it. If no, forwards it.
  • Client — the code that assembles the chain and dispatches the first request to it. Doesn't know which handler will end up handling it.

The defining property: each handler holds only a reference to the next — never a reference to the whole chain. Handlers don't know their position; they don't know the count; they only know what's downstream.

Code Examples

Here's an HTTP middleware chain in five languages. Notice how each concrete middleware is small and single-concern, and how the chain assembly happens outside the handlers themselves.

abstract class Middleware {
  private next: Middleware | null = null;

  setNext(next: Middleware): Middleware {
    this.next = next;
    return next;   // for fluent chaining
  }

  handle(req: Request): Response {
    if (this.next) return this.next.handle(req);
    return { status: 404, body: "not found" };
  }
}

class AuthMiddleware extends Middleware {
  handle(req: Request): Response {
    const user = authenticate(req);
    if (!user) return { status: 401, body: "unauthorized" };
    req.user = user;
    return super.handle(req);
  }
}

class RateLimitMiddleware extends Middleware {
  handle(req: Request): Response {
    if (isOverLimit(req.user!)) return { status: 429, body: "too many requests" };
    return super.handle(req);
  }
}

class RouteHandler extends Middleware {
  handle(req: Request): Response {
    return { status: 200, body: doTheWork(req) };
  }
}

// Assemble the chain at boot:
const pipeline = new AuthMiddleware();
pipeline.setNext(new RateLimitMiddleware()).setNext(new RouteHandler());
from abc import ABC, abstractmethod

class Middleware(ABC):
    def __init__(self):
        self._next = None

    def set_next(self, next_handler):
        self._next = next_handler
        return next_handler   # for fluent chaining

    def handle(self, req):
        if self._next:
            return self._next.handle(req)
        return Response(404, "not found")

class AuthMiddleware(Middleware):
    def handle(self, req):
        user = authenticate(req)
        if not user:
            return Response(401, "unauthorized")
        req.user = user
        return super().handle(req)

class RateLimitMiddleware(Middleware):
    def handle(self, req):
        if is_over_limit(req.user):
            return Response(429, "too many requests")
        return super().handle(req)

class RouteHandler(Middleware):
    def handle(self, req):
        return Response(200, do_the_work(req))

# Assemble:
pipeline = AuthMiddleware()
pipeline.set_next(RateLimitMiddleware()).set_next(RouteHandler())
public abstract class Middleware {
    private Middleware next;

    public Middleware setNext(Middleware next) {
        this.next = next;
        return next;   // fluent chaining
    }

    public Response handle(Request req) {
        if (next != null) return next.handle(req);
        return new Response(404, "not found");
    }
}

public final class AuthMiddleware extends Middleware {
    @Override
    public Response handle(Request req) {
        var user = authenticate(req);
        if (user == null) return new Response(401, "unauthorized");
        req.setUser(user);
        return super.handle(req);
    }
}

public final class RateLimitMiddleware extends Middleware {
    @Override
    public Response handle(Request req) {
        if (isOverLimit(req.user())) return new Response(429, "too many requests");
        return super.handle(req);
    }
}

public final class RouteHandler extends Middleware {
    @Override
    public Response handle(Request req) {
        return new Response(200, doTheWork(req));
    }
}
<?php

namespace App\Http;

abstract class Middleware
{
    private ?Middleware $next = null;

    public function setNext(Middleware $next): Middleware
    {
        $this->next = $next;
        return $next;   // fluent chaining
    }

    public function handle(Request $req): Response
    {
        return $this->next?->handle($req) ?? new Response(404, 'not found');
    }
}

final class AuthMiddleware extends Middleware
{
    public function handle(Request $req): Response
    {
        $user = authenticate($req);
        if (!$user) return new Response(401, 'unauthorized');
        $req->user = $user;
        return parent::handle($req);
    }
}

final class RateLimitMiddleware extends Middleware
{
    public function handle(Request $req): Response
    {
        if (isOverLimit($req->user)) return new Response(429, 'too many requests');
        return parent::handle($req);
    }
}

final class RouteHandler extends Middleware
{
    public function handle(Request $req): Response
    {
        return new Response(200, doTheWork($req));
    }
}
package http

// Go's idiomatic shape for CoR is composing middleware functions, not classes.
// Each middleware takes a "next" handler and returns a handler that either
// short-circuits or calls next.

type Handler func(Request) Response

func AuthMiddleware(next Handler) Handler {
    return func(req Request) Response {
        user, ok := Authenticate(req)
        if !ok {
            return Response{Status: 401, Body: "unauthorized"}
        }
        req.User = user
        return next(req)
    }
}

func RateLimitMiddleware(next Handler) Handler {
    return func(req Request) Response {
        if IsOverLimit(req.User) {
            return Response{Status: 429, Body: "too many requests"}
        }
        return next(req)
    }
}

func Route(req Request) Response {
    return Response{Status: 200, Body: DoTheWork(req)}
}

// Assemble (read inside-out — Auth runs first):
// pipeline := AuthMiddleware(RateLimitMiddleware(Route))
// resp := pipeline(req)

The Go version reveals what the inheritance-based versions can hide: Chain of Responsibility is fundamentally just function composition with an early-exit option. Each link receives the next link as input and returns a function that decides whether to forward. Modern web frameworks in every language use this exact shape — even when they call it "middleware" instead of "Chain of Responsibility."

When to Use It

Reach for Chain of Responsibility when you can answer "yes" to any of these:

  • A request needs to flow through several independent checks before being handled. HTTP middleware, validation pipelines, event filters, permission ladders.
  • The set of handlers can vary at runtime or per-environment. Production has rate-limiting; tests skip it. Staging has extra logging. Different chains for different routes.
  • Handlers are independent and reorderable. Auth before rate-limit; rate-limit before validation. Reordering means a different setNext() chain at boot — no class changes.
  • You want short-circuiting. The first handler that takes responsibility ends the chain. Useful when only one step needs to handle the request and the rest are alternatives.
  • You want the sender to be ignorant of who handles the request. The controller dispatches into the chain; what happens inside is the chain's business.

If exactly one step always handles every request and there's no short-circuiting, you don't need this pattern — just call the function. CoR earns its cost when the flow and the short-circuit are both meaningful.

Pros and Cons

Pros

  • New handlers can be added without touching existing ones — you just add a link to the chain.
  • The order of handlers is data, not code structure — change it by re-assembling the chain.
  • Each handler is small, focused, and individually testable.
  • Cross-cutting concerns (auth, logging, validation) live in handlers rather than scattered through callers.
  • Different chains for different contexts (per-route, per-environment, per-tenant) are straightforward.

Cons

  • Requests can fall off the end of the chain unhandled. Without a fallback handler, a request reaches the last link and gets nothing back. Always provide a tail.
  • Debugging "why didn't this fire?" can be hard. When a request was short-circuited two links earlier, the actual handler never sees it — and the trail goes cold quickly without good logging.
  • Order matters and isn't always visible. Auth → Validation → RateLimit and RateLimit → Auth → Validation give very different behaviour. Document the order; don't let it drift.
  • Introduces indirection for chains of one or two — overkill if there's only one handler.

Pro Tips

  • Always have a default tail handler that handles "request reached the end without anyone taking responsibility." Even if it just logs and returns a generic 404, it stops requests from quietly disappearing.
  • Assemble the chain in one place — the application bootstrap, a service provider, a factory. Chains assembled implicitly across N files are unfindable; chains assembled in one file are obvious.
  • Each handler does one check. Resist the urge to combine "auth + rate limit" into one handler. The pattern's promise is composability; combining handlers undermines it.
  • Watch for "chain-of-decorator confusion." A Decorator chain always calls through to the next layer; a CoR chain might short-circuit. If your handlers always call through, what you have is Decorators, not CoR.
  • Pass the request as immutable when possible. Mutations by middlewares are a known source of "where did this field get set?" bugs. If you must mutate, document where and which middleware does it.

Relations with Other Patterns

  • Decorator has the same wrapping shape but a key difference: a Decorator always calls through to the wrapped object, then adds behavior; a Chain of Responsibility handler might short-circuit and not call the next link at all. The intent diverges where the short-circuit lives.
  • Command can be the thing a handler in the chain produces — each request might wrap a Command that gets queued or executed only by the right handler.
  • Composite can host a chain — children passed up the parent chain until one handles. This is exactly how DOM event bubbling works.
  • Mediator also coordinates between objects, but in the opposite direction: Mediator is bidirectional (colleagues ↔ mediator); CoR is one-way through a sequence.

Final Tips

The Chain of Responsibility I've shipped most often isn't HTTP middleware — it's permission checks. A user wants to delete a comment. Are they an admin? (Allow.) Are they the comment's author? (Allow.) Are they within the 5-minute edit window of any post they own? (Allow.) Otherwise, deny. Each rule is a small handler; the chain is a list of rules; adding a new rule is a one-line change.

That's the pattern's quiet promise: when "before X happens, several conditions must each get a chance to weigh in" describes your problem, Chain of Responsibility is almost certainly the cleanest answer. Reach for it when the order of checks matters, when checks should short-circuit, and when the set of checks is open to change.