SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignChain of Responsibility Pattern
Software DesignBehavioral Patterns

Chain of Responsibility Pattern

Pass a request along a chain of handlers until one handles it — decouple sender from receiver.

April 14, 20255 min read
design-patternsjavachain-of-responsibilitybehavioral

One-line Definition

Decouples a request's sender from its receiver by passing the request along a dynamic chain of potential handlers.


Problem

Suppose you're building a tech support system. A user submits an issue. Depending on the severity, it needs to be handled by Level 1 Support, Level 2 Support, or Engineering. If you hardcode this routing in your main application using giant if-else blocks, adding a new support level breaks the Open/Closed Principle. The Chain of Responsibility pattern solves this by linking handlers into a chain; the request travels down the chain until a handler finally resolves it.


Real-world Analogy

When you file a support ticket, it first goes to Level 1 support. If they can't handle it, they escalate to Level 2. If Level 2 can't, it goes to Level 3 — the engineering team. Each level either resolves the issue or passes it up the chain. You (the sender) don't decide who handles your ticket — the chain does.


Structure

  • Client
    • Sends the request to the first handler in the chain, completely unaware of which specific handler will eventually process it.
  • Handler
    • Abstract class or interface defining the processing method and holding a reference to the next handler in the chain.
  • ConcreteHandler
    • Evaluates the request. If it can handle it, it does. If not, it forwards the request to the next handler.

Diagram

classDiagram
    class Client {
    }
    class Handler {
        <<abstract>>
        -Handler next
        +setNext(h: Handler)
        +handleRequest(request)
    }
    class ConcreteHandlerA {
        +handleRequest(request)
    }
    class ConcreteHandlerB {
        +handleRequest(request)
    }
    
    Client --> Handler : sends request
    Handler o-- Handler : next
    ConcreteHandlerA --|> Handler
    ConcreteHandlerB --|> Handler

Code Walkthrough

Notice how the Client (Main) just passes the issue to the first handler (l1). The handlers internally decide whether to process it or delegate it further down the chain.

abstract class SupportHandler {
    private SupportHandler nextHandler;

    public SupportHandler setNext(SupportHandler next) {
        this.nextHandler = next;
        return next;
    }

    public void handle(String issue, int severity) {
        if (canHandle(severity)) {
            process(issue, severity);
        } else if (nextHandler != null) {
            nextHandler.handle(issue, severity);
        } else {
            System.out.println("No handler available for: " + issue);
        }
    }

    protected abstract boolean canHandle(int severity);
    protected abstract void process(String issue, int severity);
}

class Level1Support extends SupportHandler {
    @Override
    protected boolean canHandle(int severity) { return severity == 1; }
    @Override
    protected void process(String issue, int severity) {
        System.out.println("[L1 Support] Resolved: " + issue);
    }
}

class Level2Support extends SupportHandler {
    @Override
    protected boolean canHandle(int severity) { return severity == 2; }
    @Override
    protected void process(String issue, int severity) {
        System.out.println("[L2 Support] Resolved: " + issue);
    }
}

class Level3Support extends SupportHandler {
    @Override
    protected boolean canHandle(int severity) { return severity == 3; }
    @Override
    protected void process(String issue, int severity) {
        System.out.println("[L3 Engineering] Resolved: " + issue);
    }
}

class Main {
    public static void main(String[] args) {
        SupportHandler l1 = new Level1Support();
        SupportHandler l2 = new Level2Support();
        SupportHandler l3 = new Level3Support();

        // Build the chain
        l1.setNext(l2).setNext(l3);

        // Client doesn't know who handles what
        l1.handle("Password reset", 1);
        l1.handle("Application crash", 2);
        l1.handle("Data corruption", 3);
        l1.handle("Unknown issue", 4);
    }
}

Bad vs Good

Bad Approach

Problems

  • Routing logic and handling logic are mixed together in a giant monolithic block.
  • The SupportDispatcher is heavily coupled to every single handler type.
class SupportDispatcher {
    public void handleTicket(String issue, int severity) {
        if (severity == 1) {
            System.out.println("[L1] Resolved: " + issue);
        } else if (severity == 2) {
            System.out.println("[L2] Resolved: " + issue);
        } else if (severity == 3) {
            System.out.println("[L3] Resolved: " + issue);
        } else {
            System.out.println("No handler for: " + issue);
        }
    }
}

Better Approach

Improvements

  • Each handler is entirely decoupled and has a single responsibility.
  • New handlers can be added by extending the abstract class and injecting them into the chain, keeping business logic unchanged.
// Chain construction happens once
SupportHandler l1 = new Level1Support();
SupportHandler l2 = new Level2Support();
SupportHandler l3 = new Level3Support();
l1.setNext(l2).setNext(l3);

// Client code is remarkably simple
l1.handle("Data corruption", 3);  // Passes through L1 → L2 → L3 handles

Pros vs Cons

ProsCons
Strictly decouples the sender of a request from its receiversThe request might go completely unhandled if the chain ends
Obeys the Single Responsibility Principle for each handlerCan be very difficult to debug and trace execution flow
Allows handlers to be added, removed, or reordered at runtimeDeep chains can cause slight performance degradation
Adheres to the Open/Closed Principle

When to Use

  • A request needs to be processed by multiple handlers, but you don't know the exact handler in advance.
  • You want to decouple the sender of a request from the object that actually processes it.
  • The set of handlers or their exact sequence should be dynamically configurable at runtime.

When Not to Use

  • Every request is always routed to the exact same handler (just use direct method calls).
  • Performance is absolutely critical and traversing a long chain adds unacceptable overhead.

Real-world Examples

  • javax.servlet.Filter in Java Web Applications (Servlet Filters form a chain before hitting the Controller).
  • Spring Security's SecurityFilterChain.
  • Java's java.util.logging.Logger#log() (Logs pass through a hierarchy of loggers).

Key Takeaway

The Chain of Responsibility Pattern replaces hard-coded routing logic with a dynamic chain of handlers. Use it when multiple objects might handle a request and you want to decouple the client from the routing logic. Avoid it if the routing logic is simple, static, and unlikely to change, as chains can be notoriously hard to debug.

PreviousFlyweight PatternNextFacade Pattern