Back to Design Patterns
Behavioral Pattern

Chain of Responsibility

Pass a request along a chain of handlers. Each handler decides to process the request or pass it to the next handler in the chain.

Intermediate10 min read

What is Chain of Responsibility?

Chain of Responsibility is a behavioral design pattern that lets you pass a request along a chain of handler objects. Each handler checks if it can process the request. If yes, it handles it. If no, it passes the request to the next handler.

Think of a customer support escalation: your ticket goes to a Level 1 agent first. If they can't solve it, it escalates to Level 2, then Level 3, then management. Each level either handles it or passes it on — without the customer caring who handles it.

This decouples the sender (who creates the request) from the receiver (who processes it). The sender doesn't know which handler will process it — or even how many handlers there are.

Key Idea: Build a pipeline of handlers. Each handler either processes the request or passes it to the next. The sender is completely decoupled from which handler does the work.

Core Participants

Handler Interface

Handler Interface

Declares the method for handling requests and optionally the method for setting the next handler (setNext). All concrete handlers implement this interface.

Base Handler

Base Handler (optional)

Optional abstract class that stores the next handler reference and implements the default "pass to next" behaviour. Concrete handlers extend this and only override what they need.

Concrete Handler

Concrete Handler

Checks if it can handle the request. If yes, processes it. If no, calls the next handler. Each handler has its own criteria for what it can handle.

Client

Client

Builds the chain by linking handlers with setNext(), then sends the request to the first handler. The client doesn't know which handler will ultimately process the request.

!Problem It Solves

Hardcoded Handler Logic

Without Chain of Responsibility, the sender must know all possible handlers and route to them with conditionals:

  • → Sender is tightly coupled to every handler — must know all of them
  • → Adding a new handler level requires editing the sender's routing logic
  • → Changing the order of handlers requires modifying the sender
  • → No way to dynamically build or modify the chain at runtime

Without CoR

// Sender knows about every handler — tightly coupled
void routeTicket(Ticket t) {
    if (t.priority == LOW)      l1.handle(t);
    else if (t.priority == MED) l2.handle(t);
    else if (t.priority == HIGH) l3.handle(t);
    else                         mgmt.handle(t);
    // Add new level → edit this method
}

With Chain of Responsibility

// Sender just fires — chain handles the routing
l1.process(ticket);
// l1 → l2 → l3 → management — sender doesn't care which handles it
// Add new level → add a new handler, insert into chain

Chain Flow Variants

There are two common flavours of how a chain processes requests:

Stop at First Handler

The first handler that can process the request handles it and the chain stops. Used for routing — exactly one handler should respond.

// Only the matching handler responds
l1.process(ticket);  // L1 handles LOW tickets
                     // L2+ never see it

Example: Support escalation, approval routing

All Handlers Process

Every handler in the chain processes the request in order, regardless of what others did. Used for middleware pipelines.

// Every handler runs in sequence
rateLimit → auth → logging → handler
// All run for every request

Example: HTTP middleware, event filters

Chain Flow Diagram

Request flows through the chain

Handler 1Can handle? YES→ Processes & stops
Handler 1Can handle? NO→ Passes to Handler 2
Handler 2Can handle? NO→ Passes to Handler 3
Handler 3Can handle? YES→ Processes & stops

Implementation

// Chain of Responsibility — Support Ticket Escalation

// ─── Handler interface ────────────────────────────────────────
abstract class SupportHandler {
    private SupportHandler next;

    // Fluent setter — enables chaining: h1.setNext(h2).setNext(h3)
    public SupportHandler setNext(SupportHandler next) {
        this.next = next;
        return next;
    }

    // Template method — subclasses override canHandle() and handle()
    public void process(SupportTicket ticket) {
        if (canHandle(ticket)) {
            handle(ticket);
        } else if (next != null) {
            System.out.println("[" + getClass().getSimpleName()
                + "] Escalating ticket #" + ticket.getId());
            next.process(ticket);
        } else {
            System.out.println("[Chain] No handler found for ticket #"
                + ticket.getId() + " (Priority: " + ticket.getPriority() + ")");
        }
    }

    protected abstract boolean canHandle(SupportTicket ticket);
    protected abstract void handle(SupportTicket ticket);
}

// ─── Data object ──────────────────────────────────────────────
class SupportTicket {
    private int id;
    private String priority;  // "LOW", "MEDIUM", "HIGH", "CRITICAL"
    private String issue;

    public SupportTicket(int id, String priority, String issue) {
        this.id       = id;
        this.priority = priority;
        this.issue    = issue;
    }
    public int    getId()       { return id; }
    public String getPriority() { return priority; }
    public String getIssue()    { return issue; }
}

// ─── Concrete Handlers ────────────────────────────────────────
class Level1Support extends SupportHandler {
    @Override
    protected boolean canHandle(SupportTicket t) {
        return t.getPriority().equals("LOW");
    }

    @Override
    protected void handle(SupportTicket t) {
        System.out.println("[L1 Support] Resolving ticket #" + t.getId()
            + " — " + t.getIssue());
    }
}

class Level2Support extends SupportHandler {
    @Override
    protected boolean canHandle(SupportTicket t) {
        return t.getPriority().equals("MEDIUM");
    }

    @Override
    protected void handle(SupportTicket t) {
        System.out.println("[L2 Support] Resolving ticket #" + t.getId()
            + " — " + t.getIssue());
    }
}

class Level3Support extends SupportHandler {
    @Override
    protected boolean canHandle(SupportTicket t) {
        return t.getPriority().equals("HIGH");
    }

    @Override
    protected void handle(SupportTicket t) {
        System.out.println("[L3 Engineer] Resolving ticket #" + t.getId()
            + " — " + t.getIssue());
    }
}

class ManagementSupport extends SupportHandler {
    @Override
    protected boolean canHandle(SupportTicket t) {
        return t.getPriority().equals("CRITICAL");
    }

    @Override
    protected void handle(SupportTicket t) {
        System.out.println("[Management] Handling CRITICAL ticket #" + t.getId()
            + " — " + t.getIssue());
    }
}

// ─── Client — builds the chain ────────────────────────────────
public class Application {
    public static void main(String[] args) {
        // Build chain: L1 → L2 → L3 → Management
        SupportHandler l1 = new Level1Support();
        l1.setNext(new Level2Support())
          .setNext(new Level3Support())
          .setNext(new ManagementSupport());

        // Tickets automatically routed to the right handler
        l1.process(new SupportTicket(101, "LOW",      "Password reset"));
        l1.process(new SupportTicket(102, "MEDIUM",   "App crash on login"));
        l1.process(new SupportTicket(103, "HIGH",     "Data corruption bug"));
        l1.process(new SupportTicket(104, "CRITICAL", "Production server down"));
        // Output:
        // [L1] Resolving #101
        // [L1] Escalating → [L2] Resolving #102
        // [L1] Escalating → [L2] Escalating → [L3] Resolving #103
        // [L1] Escalating → ... → [Management] Handling CRITICAL #104
    }
}

When to Use Chain of Responsibility

Request Escalation

When a request should be handled by the first capable handler in a hierarchy — support tickets, approval workflows, incident routing.

SupportEscalation, ExpenseApproval, IncidentRouter

Middleware Pipelines

When a request must pass through a series of processing steps — authentication, rate limiting, logging, caching — each step independent.

ExpressMiddleware, DjangoMiddleware, HttpFilter

Event Handling

When UI events bubble up through a component hierarchy — a click handled by a button, if not then the panel, if not then the window.

DOM event bubbling, Android touch events

Validation Pipelines

When input must pass through multiple independent validation rules — each rule either rejects or passes to the next.

FormValidator, InputSanitizer, SchemaChecker

Logging Levels

When log messages are routed to different outputs based on severity — DEBUG to console, WARNING to file, ERROR to alerting service.

ConsoleLogger, FileLogger, AlertLogger

Pros & Cons

Advantages

  • Sender is completely decoupled from all handlers
  • Add, remove, or reorder handlers without touching sender or other handlers
  • Single Responsibility — each handler does one thing
  • Chain can be built dynamically at runtime
  • Open/Closed — add new handlers without modifying existing ones

Disadvantages

  • No guarantee a request will be handled — can fall through the chain
  • Hard to debug — must trace through all handlers to find where it was processed
  • Long chains can impact performance
  • Request may be processed multiple times if not carefully designed

Real-World Examples

Java's Servlet Filters

Java Servlet Filters form a chain — each filter in web.xml is linked into a FilterChain. Each filter calls chain.doFilter() to pass the request to the next filter, or returns early to block it.

Express.js Middleware (Node.js)

app.use(middleware) builds a Chain of Responsibility. Each middleware calls next() to pass to the next one, or returns a response to stop the chain. Rate limiters, auth, CORS all use this.

Python's logging Module

Python's logging.Logger propagates log records up the logger hierarchy. Each logger in the chain can handle (with its handlers) and optionally propagate to the parent — a classic CoR implementation.

DOM Event Bubbling (Browser)

Browser events bubble up through the DOM tree — from the target element to its parent, grandparent, up to the document. Each element can handle or let it bubble — Chain of Responsibility in the browser.

What's Next?

Now that you understand Chain of Responsibility, explore related Behavioral patterns: