Chain of Responsibility Pattern Explained

Updated 2026-03-11

TL;DR

The Chain of Responsibility pattern passes a request along a chain of handlers until one handles it. Each handler decides whether to process the request or pass it to the next handler. This decouples senders from receivers and allows multiple objects a chance to handle the request.

Prerequisites: Understanding of classes and objects, inheritance and polymorphism, abstract base classes (ABC module in Python), and basic understanding of behavioral design patterns. Familiarity with method overriding and interface implementation.

After this topic: Implement a chain of responsibility pattern to process requests through multiple handlers, design flexible request processing pipelines, identify when to use this pattern in real-world scenarios, and explain the pattern’s benefits and trade-offs in technical interviews.

Core Concept

What is Chain of Responsibility?

The Chain of Responsibility pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

Core Components

The pattern consists of three main parts:

  1. Handler Interface: Defines the interface for handling requests and optionally storing the next handler
  2. Concrete Handlers: Implement the handling logic and decide whether to process or forward
  3. Client: Initiates the request to the first handler in the chain

How It Works

Each handler in the chain has a reference to the next handler. When a request arrives, the handler either:

  • Processes it completely and stops the chain
  • Processes it partially and passes it forward
  • Doesn’t process it at all and passes it forward
  • Processes it and still passes it forward (less common)

The chain can be dynamic — handlers can be added, removed, or reordered at runtime.

Why Use This Pattern?

Decoupling: The sender doesn’t need to know which handler will ultimately process the request. This reduces coupling between request senders and receivers.

Flexibility: You can add or remove handlers without changing existing code. The order of handlers can be changed easily.

Single Responsibility: Each handler focuses on one type of processing, making the code more maintainable.

Real-World Analogies

Think of a customer support system: a question first goes to a chatbot, then to a junior support agent, then to a senior agent, and finally to a specialist. Each level tries to answer, and if they can’t, they escalate to the next level.

Other examples include middleware in web frameworks, event bubbling in UI systems, and approval workflows in organizations.

Visual Guide

Chain of Responsibility Structure

classDiagram
    class Handler {
        <<abstract>>
        +Handler next_handler
        +set_next(handler) Handler
        +handle(request)* void
    }
    class ConcreteHandlerA {
        +handle(request) void
    }
    class ConcreteHandlerB {
        +handle(request) void
    }
    class ConcreteHandlerC {
        +handle(request) void
    }
    class Client
    
    Handler <|-- ConcreteHandlerA
    Handler <|-- ConcreteHandlerB
    Handler <|-- ConcreteHandlerC
    Handler o-- Handler : next
    Client --> Handler

The Handler interface defines the chain structure. Each concrete handler can process the request or pass it to the next handler.

Request Flow Through Chain

sequenceDiagram
    participant Client
    participant HandlerA
    participant HandlerB
    participant HandlerC
    
    Client->>HandlerA: handle(request)
    alt Can handle
        HandlerA->>HandlerA: Process request
        HandlerA-->>Client: Done
    else Cannot handle
        HandlerA->>HandlerB: handle(request)
        alt Can handle
            HandlerB->>HandlerB: Process request
            HandlerB-->>Client: Done
        else Cannot handle
            HandlerB->>HandlerC: handle(request)
            HandlerC->>HandlerC: Process request
            HandlerC-->>Client: Done
        end
    end

The request flows through the chain until a handler processes it. If no handler can process it, the request may go unhandled (or a default handler catches it).

Examples

Example 1: Support Ticket System

Let’s build a customer support system where tickets are routed based on priority.

from abc import ABC, abstractmethod
from enum import Enum

class Priority(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3
    CRITICAL = 4

class SupportTicket:
    def __init__(self, priority: Priority, description: str):
        self.priority = priority
        self.description = description

# Handler Interface
class SupportHandler(ABC):
    def __init__(self):
        self._next_handler = None
    
    def set_next(self, handler):
        """Set the next handler in the chain."""
        self._next_handler = handler
        return handler  # Return for chaining: h1.set_next(h2).set_next(h3)
    
    @abstractmethod
    def handle(self, ticket: SupportTicket):
        """Handle the ticket or pass to next handler."""
        pass

# Concrete Handlers
class JuniorSupport(SupportHandler):
    def handle(self, ticket: SupportTicket):
        if ticket.priority == Priority.LOW:
            print(f"Junior Support: Handling '{ticket.description}'")
            return True
        elif self._next_handler:
            return self._next_handler.handle(ticket)
        return False

class SeniorSupport(SupportHandler):
    def handle(self, ticket: SupportTicket):
        if ticket.priority in [Priority.MEDIUM, Priority.HIGH]:
            print(f"Senior Support: Handling '{ticket.description}'")
            return True
        elif self._next_handler:
            return self._next_handler.handle(ticket)
        return False

class Manager(SupportHandler):
    def handle(self, ticket: SupportTicket):
        if ticket.priority == Priority.CRITICAL:
            print(f"Manager: Handling '{ticket.description}'")
            return True
        elif self._next_handler:
            return self._next_handler.handle(ticket)
        print(f"No one could handle: '{ticket.description}'")
        return False

# Client code
if __name__ == "__main__":
    # Build the chain
    junior = JuniorSupport()
    senior = SeniorSupport()
    manager = Manager()
    
    junior.set_next(senior).set_next(manager)
    
    # Create tickets
    tickets = [
        SupportTicket(Priority.LOW, "Password reset"),
        SupportTicket(Priority.HIGH, "Server down"),
        SupportTicket(Priority.CRITICAL, "Data breach"),
        SupportTicket(Priority.MEDIUM, "Feature request")
    ]
    
    # Process tickets
    for ticket in tickets:
        print(f"\nProcessing: {ticket.description} (Priority: {ticket.priority.name})")
        junior.handle(ticket)

Expected Output:

Processing: Password reset (Priority: LOW)
Junior Support: Handling 'Password reset'

Processing: Server down (Priority: HIGH)
Senior Support: Handling 'Server down'

Processing: Data breach (Priority: CRITICAL)
Manager: Handling 'Data breach'

Processing: Feature request (Priority: MEDIUM)
Senior Support: Handling 'Feature request'

Try it yourself: Add a new handler for “TeamLead” that handles HIGH priority tickets, and modify SeniorSupport to only handle MEDIUM priority.

Example 2: Authentication Middleware Chain

Web frameworks often use chain of responsibility for request processing. Here’s a simplified authentication system:

from abc import ABC, abstractmethod
from typing import Optional

class Request:
    def __init__(self, username: str, password: str, role: str = None):
        self.username = username
        self.password = password
        self.role = role
        self.authenticated = False

class Middleware(ABC):
    def __init__(self):
        self._next: Optional[Middleware] = None
    
    def set_next(self, middleware):
        self._next = middleware
        return middleware
    
    @abstractmethod
    def check(self, request: Request) -> bool:
        """Return True if request should continue, False to stop."""
        pass

class AuthenticationMiddleware(Middleware):
    def __init__(self, valid_users: dict):
        super().__init__()
        self._valid_users = valid_users  # {username: password}
    
    def check(self, request: Request) -> bool:
        print(f"[Auth] Checking credentials for {request.username}...")
        
        if request.username not in self._valid_users:
            print(f"[Auth] ❌ User not found")
            return False
        
        if self._valid_users[request.username] != request.password:
            print(f"[Auth] ❌ Invalid password")
            return False
        
        print(f"[Auth] ✓ Authenticated")
        request.authenticated = True
        
        if self._next:
            return self._next.check(request)
        return True

class RoleCheckMiddleware(Middleware):
    def __init__(self, required_role: str):
        super().__init__()
        self._required_role = required_role
    
    def check(self, request: Request) -> bool:
        print(f"[Role] Checking if user has '{self._required_role}' role...")
        
        if request.role != self._required_role:
            print(f"[Role] ❌ Insufficient permissions")
            return False
        
        print(f"[Role] ✓ Role verified")
        
        if self._next:
            return self._next.check(request)
        return True

class RateLimitMiddleware(Middleware):
    def __init__(self, max_requests: int):
        super().__init__()
        self._max_requests = max_requests
        self._request_count = {}
    
    def check(self, request: Request) -> bool:
        print(f"[RateLimit] Checking request count...")
        
        count = self._request_count.get(request.username, 0)
        if count >= self._max_requests:
            print(f"[RateLimit] ❌ Too many requests ({count}/{self._max_requests})")
            return False
        
        self._request_count[request.username] = count + 1
        print(f"[RateLimit] ✓ Request allowed ({count + 1}/{self._max_requests})")
        
        if self._next:
            return self._next.check(request)
        return True

# Client code
if __name__ == "__main__":
    # Setup
    users = {"alice": "pass123", "bob": "secret456"}
    
    auth = AuthenticationMiddleware(users)
    rate_limit = RateLimitMiddleware(max_requests=3)
    role_check = RoleCheckMiddleware(required_role="admin")
    
    # Build chain: auth -> rate_limit -> role_check
    auth.set_next(rate_limit).set_next(role_check)
    
    # Test cases
    print("=== Test 1: Valid admin user ===")
    req1 = Request("alice", "pass123", "admin")
    result = auth.check(req1)
    print(f"Result: {'✓ Allowed' if result else '❌ Denied'}\n")
    
    print("=== Test 2: Wrong password ===")
    req2 = Request("alice", "wrong", "admin")
    result = auth.check(req2)
    print(f"Result: {'✓ Allowed' if result else '❌ Denied'}\n")
    
    print("=== Test 3: Non-admin user ===")
    req3 = Request("bob", "secret456", "user")
    result = auth.check(req3)
    print(f"Result: {'✓ Allowed' if result else '❌ Denied'}\n")
    
    print("=== Test 4: Rate limit (4th request) ===")
    for i in range(4):
        print(f"\n--- Request {i+1} ---")
        req = Request("alice", "pass123", "admin")
        result = auth.check(req)
        print(f"Result: {'✓ Allowed' if result else '❌ Denied'}")

Expected Output:

=== Test 1: Valid admin user ===
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (1/3)
[Role] Checking if user has 'admin' role...
[Role] ✓ Role verified
Result: ✓ Allowed

=== Test 2: Wrong password ===
[Auth] Checking credentials for alice...
[Auth] ❌ Invalid password
Result: ❌ Denied

=== Test 3: Non-admin user ===
[Auth] Checking credentials for bob...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (1/3)
[Role] Checking if user has 'admin' role...
[Role] ❌ Insufficient permissions
Result: ❌ Denied

=== Test 4: Rate limit (4th request) ===

--- Request 1 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (2/3)
[Role] Checking if user has 'admin' role...
[Role] ✓ Role verified
Result: ✓ Allowed

--- Request 2 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ✓ Request allowed (3/3)
[Role] Checking if user has 'admin' role...
[Role] ✓ Role verified
Result: ✓ Allowed

--- Request 3 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ❌ Too many requests (3/3)
Result: ❌ Denied

--- Request 4 ---
[Auth] Checking credentials for alice...
[Auth] ✓ Authenticated
[RateLimit] Checking request count...
[RateLimit] ❌ Too many requests (3/3)
Result: ❌ Denied

Try it yourself: Add a LoggingMiddleware that logs all requests before authentication. Place it at the start of the chain.

Java/C++ Notes

Java: Use interfaces or abstract classes for the Handler. The pattern looks similar:

public abstract class Handler {
    private Handler next;
    
    public Handler setNext(Handler next) {
        this.next = next;
        return next;
    }
    
    public abstract boolean handle(Request request);
}

C++: Use virtual functions and pointers:

class Handler {
protected:
    Handler* next = nullptr;
public:
    Handler* setNext(Handler* handler) {
        next = handler;
        return handler;
    }
    virtual bool handle(Request& request) = 0;
    virtual ~Handler() = default;
};

Common Mistakes

1. Breaking the Chain Accidentally

Mistake: Forgetting to call the next handler, which breaks the chain.

class BadHandler(SupportHandler):
    def handle(self, ticket):
        if ticket.priority == Priority.LOW:
            print("Handling low priority")
            return True
        # Forgot to call self._next_handler.handle(ticket)!
        return False

Why it’s wrong: Requests that this handler can’t process will be dropped instead of forwarded.

Fix: Always check if there’s a next handler and call it:

if self._next_handler:
    return self._next_handler.handle(ticket)

2. Not Checking for None Before Calling Next

Mistake: Calling self._next_handler.handle() without checking if _next_handler is None.

def handle(self, request):
    if not self.can_handle(request):
        return self._next_handler.handle(request)  # May be None!

Why it’s wrong: This causes an AttributeError when the handler is the last in the chain.

Fix: Always check:

if self._next_handler:
    return self._next_handler.handle(request)
return False  # Or handle the "no handler found" case

3. Creating Circular Chains

Mistake: Accidentally creating a loop in the chain.

handler1.set_next(handler2)
handler2.set_next(handler3)
handler3.set_next(handler1)  # Creates a cycle!

Why it’s wrong: Requests will loop infinitely, causing stack overflow or infinite loops.

Fix: Be careful when building chains. Consider adding cycle detection in development:

def set_next(self, handler):
    # Check for cycles (optional, for debugging)
    current = handler
    while current:
        if current is self:
            raise ValueError("Circular chain detected!")
        current = current._next_handler
    self._next_handler = handler
    return handler

4. Handlers Doing Too Much

Mistake: Making handlers responsible for multiple unrelated concerns.

class OverloadedHandler(Handler):
    def handle(self, request):
        # Validates request
        # Logs request
        # Checks permissions
        # Processes business logic
        # Sends notifications
        # All in one handler!

Why it’s wrong: Violates Single Responsibility Principle and makes the chain less flexible.

Fix: Split into multiple focused handlers:

validation = ValidationHandler()
logging = LoggingHandler()
auth = AuthorizationHandler()
processor = ProcessingHandler()

validation.set_next(logging).set_next(auth).set_next(processor)

5. Not Handling the “No Handler Found” Case

Mistake: Not having a default handler or fallback when no handler in the chain can process the request.

# Request goes through entire chain but no one handles it
# Silently fails or returns False with no logging

Why it’s wrong: Makes debugging difficult and can lead to silent failures.

Fix: Add a default handler at the end of the chain:

class DefaultHandler(Handler):
    def handle(self, request):
        print(f"Warning: No handler found for request: {request}")
        # Log, raise exception, or return default response
        return False

# Add to end of chain
handler1.set_next(handler2).set_next(handler3).set_next(DefaultHandler())

Interview Tips

When to Mention Chain of Responsibility

In interviews, bring up this pattern when you hear:

  • “Multiple objects might handle a request”
  • “Processing pipeline” or “middleware”
  • “Request routing” or “escalation”
  • “Validation chain” or “filter chain”

Key Points to Emphasize

1. Decoupling sender and receiver: Explain that the client doesn’t need to know which handler will process the request. Say: “The sender is decoupled from receivers — it just sends to the first handler, and the chain figures out who handles it.”

2. Runtime flexibility: Mention that you can add, remove, or reorder handlers at runtime without changing client code.

3. Single Responsibility: Each handler has one clear job. This makes the system easier to test and maintain.

Common Interview Questions

Q: “What’s the difference between Chain of Responsibility and Decorator?”

A: “Both chain objects together, but they have different purposes. Chain of Responsibility passes a request along until ONE handler processes it (or none do). Decorator wraps an object to ADD behavior — all decorators execute. In CoR, only one handler typically processes the request. In Decorator, all wrappers add their behavior.”

Q: “What if no handler can process the request?”

A: “You have several options: (1) Add a default handler at the end of the chain that handles all unprocessed requests, (2) Return a failure status or None, (3) Throw an exception. The choice depends on whether an unhandled request is an error condition or an expected possibility.”

Q: “When would you NOT use this pattern?”

A: “Don’t use it when: (1) You know exactly which object should handle the request — just call it directly, (2) The order of processing doesn’t matter and all handlers should process the request — use Observer pattern instead, (3) Performance is critical and the chain is very long — the overhead of traversing the chain might be significant.”

Code Challenge Preparation

Be ready to implement a chain in 10-15 minutes. Practice these scenarios:

  1. Logging system with different log levels (DEBUG, INFO, WARN, ERROR)
  2. Expense approval workflow (Manager → Director → VP → CEO based on amount)
  3. Input validation chain (NotNull → Format → Range → Business Rules)

What Interviewers Look For

  • Proper abstraction: Do you create a base Handler class/interface?
  • Chain building: Can you fluently build the chain with set_next() returning the handler?
  • Edge cases: Do you handle the last handler in the chain (checking for None)?
  • Flexibility: Can you explain how to modify the chain at runtime?

Pro Tip

When explaining the pattern, draw a simple diagram showing the chain flow. Interviewers appreciate visual thinkers. Say: “Let me sketch the flow…” and draw:

Client → HandlerA → HandlerB → HandlerC → DefaultHandler
           ↓           ↓           ↓
        Can't      Can't      Handles!
        handle     handle

This shows you understand the pattern deeply and can communicate it clearly.

Key Takeaways

  • Chain of Responsibility passes requests along a chain of handlers, where each handler decides to process or forward the request, decoupling senders from receivers
  • The pattern consists of a Handler interface with a reference to the next handler, Concrete Handlers that implement processing logic, and a Client that initiates requests
  • Use this pattern for request routing, validation pipelines, middleware systems, and approval workflows where multiple objects might handle a request
  • Always check if next_handler exists before calling it, avoid circular chains, and consider adding a default handler at the end to catch unprocessed requests
  • The pattern provides runtime flexibility — you can add, remove, or reorder handlers without changing client code, making it ideal for configurable processing pipelines