Chapter 05 · Article 27 of 55

Chain of Responsibility Pattern

Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request al…

Article outline15 sections on this page

Overview

Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

Category: Behavioural Design Pattern

Also Known As: Chain of Command, Responsibility Chain

The Chain of Responsibility pattern decouples request senders from receivers by allowing multiple objects to handle a request without the sender knowing which object will ultimately process it. Each handler in the chain either processes the request or forwards it to the next handler, creating a flexible pipeline of potential processors.

The pattern promotes loose coupling and adheres to the Single Responsibility Principle - each handler focuses solely on its own processing logic and the decision of whether to pass the request along.


Problem It Solves

Without this pattern, request handling often devolves into rigid, tightly-coupled structures:

  • Hard-coded request handling: The sender must know exactly which object handles which type of request, creating direct dependencies between sender and all possible receivers.
  • Monolithic if-else/switch chains: A single method accumulates branching logic for every possible request type, violating the Open/Closed Principle. Adding a new handler requires modifying existing code.
  • Inflexible processing pipelines: When multiple processors must act on a request in sequence (validation, authentication, logging), embedding them in a fixed order makes reordering or skipping steps difficult.
  • God objects: One class becomes responsible for routing all requests, growing into an unmaintainable monolith.
// Anti-pattern: monolithic handler
function handleRequest(request) {
    if (request.type == "auth")       { /* auth logic */ }
    else if (request.type == "rate")  { /* rate limit logic */ }
    else if (request.type == "log")   { /* logging logic */ }
    else if (request.type == "data")  { /* business logic */ }
    // Every new concern = modify this function
}

The Chain of Responsibility eliminates this by distributing handling logic across independent, composable handler objects.


When to Use / When NOT to Use

When to UseWhen NOT to Use
Multiple objects may handle a request, and the handler isn't known a prioriA single, well-known handler always processes the request
You want to issue a request without specifying the receiver explicitlyThe processing order must be strictly guaranteed and never change
The set of handlers should be configurable dynamically at runtimeEvery request MUST be handled (unless you add a fallback)
You need a processing pipeline (middleware, filters)Performance is critical and chain traversal overhead is unacceptable
You want to decouple sender from receiverThe chain would contain only one handler (over-engineering)
Request handling logic changes frequently or varies by contextHandlers need bidirectional communication with the sender
You want to apply the Open/Closed Principle to request processingYou need guaranteed response time (long chains add latency)

Key Concepts & Theory

Handler Interface

Defines a common interface with two responsibilities:

  1. handle(request) - Process the request or delegate to the next handler.
  2. setNext(handler) - Establish the successor link in the chain.

Successor Chain

Each handler holds a reference to the next handler. This forms a singly-linked list of processors. The client only knows about the first handler in the chain.

Request Propagation

When a handler receives a request, it decides:

  • Handle it - Process and (optionally) stop propagation.
  • Pass it along - Forward to the next handler in the chain.
  • Both - Process and still forward (impure chain).

Pure Chain vs Impure Chain

  • Pure Chain: Exactly one handler processes the request. Once handled, propagation stops. Example: exception handling - only the first matching catch block executes.
  • Impure Chain: Multiple handlers may process the same request. Each handler decides independently whether to process AND whether to forward. Example: servlet filters - every filter in the chain processes the request.

Base Handler (Template)

A common abstract base class implements the chaining mechanics (storing the next handler, forwarding logic), so concrete handlers only implement their specific processing logic.


ASCII Class Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                            CLIENT                                    │
│  - Builds the chain                                                 │
│  - Sends request to first handler                                   │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ sends request
                               ▼
┌──────────────────────────────────────────────────────────────┐
│                    <<interface>> Handler                       │
├──────────────────────────────────────────────────────────────┤
│  + setNext(handler: Handler): Handler                        │
│  + handle(request: Request): Result                          │
└──────────────────────────────┬───────────────────────────────┘
                               │ implements
              ┌────────────────┼────────────────┐
              ▼                ▼                 ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ConcreteHandlerA │ │ ConcreteHandlerB │ │ ConcreteHandlerC │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ - next: Handler  │ │ - next: Handler  │ │ - next: Handler  │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ + setNext()      │ │ + setNext()      │ │ + setNext()      │
│ + handle()       │ │ + handle()       │ │ + handle()       │
└────────┬─────────┘ └────────┬─────────┘ └──────────────────┘
         │   next              │   next
         └────────────────────►└──────────────────────►
         
Chain: A ──next──► B ──next──► C ──next──► null

Pseudocode Implementation

Example 1: Support Ticket Escalation (Pure Chain)

Each support level handles tickets up to a certain severity. If it cannot handle, it escalates to the next level.

enum Severity { LOW, MEDIUM, HIGH, CRITICAL }

interface SupportHandler {
    setNext(handler: SupportHandler): SupportHandler
    handle(ticket: Ticket): string
}

abstract class BaseSupportHandler implements SupportHandler {
    private next: SupportHandler = null

    setNext(handler: SupportHandler): SupportHandler {
        this.next = handler
        return handler  // enables chaining: a.setNext(b).setNext(c)
    }

    handle(ticket: Ticket): string {
        if (this.next != null) {
            return this.next.handle(ticket)
        }
        return "No handler available for ticket: " + ticket.id
    }
}

class Level1Support extends BaseSupportHandler {
    handle(ticket: Ticket): string {
        if (ticket.severity == LOW) {
            return "Level1: Resolved ticket " + ticket.id + " (password reset, FAQ)"
        }
        return super.handle(ticket)  // escalate
    }
}

class Level2Support extends BaseSupportHandler {
    handle(ticket: Ticket): string {
        if (ticket.severity == MEDIUM) {
            return "Level2: Resolved ticket " + ticket.id + " (config issue, bug)"
        }
        return super.handle(ticket)
    }
}

class Manager extends BaseSupportHandler {
    handle(ticket: Ticket): string {
        if (ticket.severity == HIGH) {
            return "Manager: Resolved ticket " + ticket.id + " (escalated complaint)"
        }
        return super.handle(ticket)
    }
}

class Director extends BaseSupportHandler {
    handle(ticket: Ticket): string {
        if (ticket.severity == CRITICAL) {
            return "Director: Resolved ticket " + ticket.id + " (system outage, legal)"
        }
        return super.handle(ticket)
    }
}

// Client builds the chain
level1 = new Level1Support()
level1.setNext(new Level2Support())
      .setNext(new Manager())
      .setNext(new Director())

// Usage
print(level1.handle(Ticket(id=101, severity=LOW)))       // Level1 handles
print(level1.handle(Ticket(id=102, severity=CRITICAL)))  // Escalates to Director

Example 2: Middleware Pipeline (Impure Chain)

Each middleware processes the request AND passes it to the next handler. Any middleware can short-circuit the chain (e.g., auth failure).

interface Middleware {
    setNext(middleware: Middleware): Middleware
    process(request: Request, response: Response): void
}

abstract class BaseMiddleware implements Middleware {
    private next: Middleware = null

    setNext(middleware: Middleware): Middleware {
        this.next = middleware
        return middleware
    }

    processNext(request: Request, response: Response): void {
        if (this.next != null) {
            this.next.process(request, response)
        }
    }
}

class AuthMiddleware extends BaseMiddleware {
    process(request: Request, response: Response): void {
        if (!request.hasValidToken()) {
            response.status = 401
            response.body = "Unauthorized"
            return  // short-circuit: do NOT call processNext
        }
        request.user = decodeToken(request.token)
        this.processNext(request, response)  // continue chain
    }
}

class RateLimitMiddleware extends BaseMiddleware {
    process(request: Request, response: Response): void {
        if (rateLimiter.isExceeded(request.ip)) {
            response.status = 429
            response.body = "Too Many Requests"
            return  // short-circuit
        }
        this.processNext(request, response)
    }
}

class LoggingMiddleware extends BaseMiddleware {
    process(request: Request, response: Response): void {
        log("Incoming: " + request.method + " " + request.path)
        this.processNext(request, response)
        log("Outgoing: " + response.status)  // post-processing
    }
}

class RequestHandler extends BaseMiddleware {
    process(request: Request, response: Response): void {
        response.status = 200
        response.body = businessLogic(request)
        // Terminal handler  -  does not call processNext
    }
}

// Client assembles the pipeline
pipeline = new AuthMiddleware()
pipeline.setNext(new RateLimitMiddleware())
        .setNext(new LoggingMiddleware())
        .setNext(new RequestHandler())

pipeline.process(incomingRequest, new Response())

Pure Chain vs Impure Chain

AspectPure ChainImpure Chain
Handlers that processExactly oneOne or more (potentially all)
After handlingPropagation stopsPropagation may continue
GuaranteeAt most one handler actsMultiple handlers may act
Short-circuitingImplicit (handler consumes request)Explicit (handler decides not to forward)
Use caseEvent handling, exception catching, escalationMiddleware pipelines, filter chains, logging
ExampleSupport ticket escalation, ATM dispensingServlet filters, Express.js middleware
RiskRequest may go unhandledOrdering bugs, duplicate processing
Control flowLinear, first-match-winsLinear, all-match-process

Real-World Examples

1. Servlet Filters (Java EE)

The FilterChain in Java Servlet specification is a textbook impure chain. Each Filter calls chain.doFilter(request, response) to pass control to the next filter. Filters handle authentication, CORS, compression, and logging.

2. Middleware Stacks (Express.js / Django)

Express.js middleware functions receive (req, res, next). Calling next() passes to the next middleware. Django's middleware classes implement process_request and process_response hooks in a layered chain.

3. Exception Handling (try-catch blocks)

Multiple catch blocks form a pure chain. The runtime walks the chain top-to-bottom; the first matching catch block handles the exception, and propagation stops.

4. Event Bubbling in DOM

When a click event fires on a nested element, it bubbles up through parent elements. Each element's event listener can handle the event and optionally call stopPropagation() to halt the chain.

5. Approval Workflows

Purchase order approvals: amounts under $1,000 are approved by a team lead, under $10,000 by a department head, under $100,000 by a VP, and above by the CFO. Each level checks its authority threshold.

6. Logging Frameworks

Log4j/SLF4J appenders form chains. A log message passes through multiple appenders (console, file, remote) - each decides independently whether to process based on log level.

7. ATM Cash Dispensing

Dispense $100 bills first, then $50, then $20, then $10. Each denomination handler dispenses what it can and passes the remainder to the next.


Chain of Responsibility vs Decorator vs Composite

AspectChain of ResponsibilityDecoratorComposite
IntentPass request until one (or more) handlers process itAdd behaviour to an object dynamicallyTreat individual and composite objects uniformly
StructureLinear linked list of handlersNested wrappers around a core objectTree hierarchy
Flow controlHandler can stop or continue the chainAlways delegates to the wrapped objectRecursively traverses children
Who processesOne handler (pure) or many (impure)All decorators + core objectAll nodes in subtree
CouplingHandlers are independent, unaware of chain structureEach decorator knows its wrappeeParent knows children
TerminationWhen a handler consumes the request or chain endsAlways reaches the core componentLeaf nodes terminate recursion
ExampleMiddleware pipelineBufferedInputStream wrapping FileInputStreamFile system (files + directories)
RelationshipHandlers are peersDecorators wrap a single subjectParent-child hierarchy

Advantages & Disadvantages

AdvantagesDisadvantages
Decouples sender and receiver - sender doesn't know who handlesNo guarantee of handling - request may fall off the chain unprocessed
Open/Closed Principle - add new handlers without modifying existing onesDebugging difficulty - hard to trace which handler processed a request
Single Responsibility - each handler has one concernPerformance overhead - long chains add latency from traversal
Dynamic configuration - chain can be assembled/modified at runtimeOrdering sensitivity - incorrect handler order produces wrong behaviour
Flexible processing - easily reorder, add, or remove handlersRuntime errors - misconfigured chains may cause infinite loops or dropped requests
Reduces conditional complexity - eliminates large if-else blocksComplexity - simple scenarios don't warrant the pattern overhead
Reusability - handlers can be shared across different chainsTesting - integration testing requires assembling chains

Constraints & Edge Cases

Unhandled Requests

If no handler in the chain matches, the request silently disappears. Mitigations:

  • Add a fallback/default handler at the end of the chain that logs or throws.
  • Return a sentinel value indicating "not handled" so the client can react.

Circular Chains

If handler A points to B, B points to C, and C points back to A, the chain loops infinitely. Mitigations:

  • Validate chain construction (detect cycles via visited-set).
  • Set a maximum hop count; abort after N forwards.

Performance with Long Chains

Each handler adds a method call and conditional check. For latency-sensitive paths:

  • Keep chains short (< 10 handlers).
  • Profile and consider collapsing rarely-separated handlers.
  • Use indexed dispatch (HashMap) if the routing key is known upfront.

Dynamic Chain Modification

Modifying the chain while a request is in-flight can cause skipped or repeated handlers. Mitigations:

  • Use immutable chain snapshots per request.
  • Apply modifications only between requests (double-buffering).

Thread Safety

In concurrent systems, shared mutable chain references require synchronization. Consider thread-local chains or copy-on-write semantics.


Interview Follow-ups

Q1: How would you ensure every request gets handled in a Chain of Responsibility?

A: Add a terminal/fallback handler at the end of the chain that always processes the request - either by returning a default response, logging an error, or throwing an exception. This guarantees no request silently falls off the chain. In middleware pipelines, the final handler is typically the actual request processor (controller/route handler).

Q2: How does Chain of Responsibility differ from the Observer pattern?

A: In Chain of Responsibility, a request travels sequentially through handlers until one (or more) processes it - it's a one-to-one-of-many relationship. In Observer, a notification is broadcast to ALL subscribers simultaneously - it's a one-to-many relationship. CoR is sequential and conditional; Observer is parallel and unconditional.

Q3: Can you combine Chain of Responsibility with other patterns?

A: Yes, commonly:

  • With Composite: Handlers can be composite objects whose children form sub-chains.
  • With Command: The request object passed through the chain can be a Command, encapsulating all context needed for processing.
  • With Decorator: Decorators always forward; CoR handlers may stop. They can coexist when some chain links always process (decorator-like) and others conditionally stop.

Hint-Only Questions

Q4: How would you implement priority-based handling where certain handlers get first chance regardless of chain order?

Hints:

  • Think about sorting handlers by priority before assembling the chain.
  • Consider a dispatcher that maintains a priority queue of handlers rather than a fixed linked list.

Q5: How would you handle a scenario where a handler needs to "undo" its processing if a downstream handler fails?

Hints:

  • Think about the Saga pattern or compensating transactions.
  • Consider wrapping the chain in a two-phase approach: forward pass (process) and backward pass (rollback on failure), similar to how servlet filters have pre/post processing around doFilter.

Counter Questions to Ask Interviewer

  1. "Should exactly one handler process the request, or can multiple handlers act on it?" - Determines pure vs impure chain, which fundamentally changes the design.

  2. "Is the chain static (configured at startup) or dynamic (modified at runtime)?" - Affects whether you need immutable snapshots, thread safety, or a chain builder.

  3. "What happens if no handler can process the request?" - Clarifies whether you need a fallback handler, exception, or silent drop.

  4. "Are there performance/latency constraints on the processing pipeline?" - Determines whether a chain is appropriate or if indexed dispatch is better.

  5. "Do handlers need access to the results of previous handlers?" - Influences whether you pass a mutable context object through the chain or use return values.


References & Whitepapers

  1. Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: Chain of Responsibility, pp. 223-232.

  2. Java Servlet Specification (Jakarta EE) - javax.servlet.FilterChain interface. Defines the standard filter chain mechanism for web applications. Jakarta Servlet 6.0 Specification.

  3. Express.js Middleware Documentation - Describes the next() function pattern for middleware chaining. expressjs.com/en/guide/using-middleware.

  4. Django Middleware Documentation - Layered middleware processing with process_request/process_response hooks. docs.djangoproject.com/en/stable/topics/http/middleware/.

  5. Fowler, M. - Patterns of Enterprise Application Architecture (2002). Discusses intercepting filters and pipeline patterns in enterprise contexts.

  6. Grand, M. - Patterns in Java, Volume 1 (2002). Extended treatment of Chain of Responsibility with Java-specific idioms.