Published on

[Behavioral] Chain of Responsibility

The Chain of Responsibility design pattern is a behavioral pattern that allows a request to be passed along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.


πŸ”— Intent

To decouple the sender of a request from its receivers by giving multiple objects a chance to handle the request.


🧱 Structure

Client β†’ Handler1 β†’ Handler2 β†’ Handler3 β†’ ...
  • Handler: Declares the interface for handling requests and optionally maintains a reference to the next handler.
  • ConcreteHandler: Handles the request or forwards it to the next handler.
  • Client: Initiates the request and sends it to the first handler in the chain.

🐍 Python Example

from typing import Optional

class Handler:
    def __init__(self, successor: Optional['Handler'] = None):
        self.successor = successor

    def handle(self, request: str):
        if self.successor:
            return self.successor.handle(request)
        return f"No handler could handle the request: {request}"

class AuthHandler(Handler):
    def handle(self, request: str):
        if request == "auth":
            return "AuthHandler: Request handled"
        return super().handle(request)

class LoggingHandler(Handler):
    def handle(self, request: str):
        if request == "log":
            return "LoggingHandler: Request handled"
        return super().handle(request)

class DataHandler(Handler):
    def handle(self, request: str):
        if request == "data":
            return "DataHandler: Request handled"
        return super().handle(request)

# Build chain: Auth β†’ Logging β†’ Data
chain = AuthHandler(LoggingHandler(DataHandler()))

# Send requests
print(chain.handle("auth"))   # AuthHandler handles it
print(chain.handle("log"))    # LoggingHandler handles it
print(chain.handle("data"))   # DataHandler handles it
print(chain.handle("unknown"))# No handler handles it

βœ… Output

AuthHandler: Request handled
LoggingHandler: Request handled
DataHandler: Request handled
No handler could handle the request: unknown

βœ… Use Cases

  • Middleware pipelines (e.g., HTTP request processing)
  • Event handling systems
  • Logging frameworks
  • Input validation chains

πŸ”„ Pros

  • Reduces coupling between sender and receiver
  • Allows adding or reordering handlers easily
  • Promotes open/closed principle

⚠️ Cons

  • Can be hard to debug if the chain becomes long or complex
  • A request might go unhandled if not all cases are covered

Real world example

For example, you have three payment methods (A, B and C) setup in your account; each having a different amount in it. A has 100 USD, B has 300 USD and C having 1000 USD and the preference for payments is chosen as A then B then C. You try to purchase something that is worth 210 USD. Using Chain of Responsibility, first of all account A will be checked if it can make the purchase, if yes purchase will be made and the chain will be broken. If not, request will move forward to account B checking for amount if yes chain will be broken otherwise the request will keep forwarding till it finds the suitable handler. Here A, B and C are links of the chain and the whole phenomenon is Chain of Responsibility.

In plain words

It helps building a chain of objects. Request enters from one end and keeps going from object to object till it finds the suitable handler.

Wikipedia says

In object-oriented design, the chain-of-responsibility pattern is a design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain.

Programmatic Example

First of all we have a base account having the logic for chaining the accounts together and some accounts

abstract class Account {
    protected successor: Account;
    protected balance: number;

    setNext(account: Account): void {
        this.successor = account;
    }

    canPay(amount): boolean {
        return this.balance >= amount;
    }

    getBalance(): number {
        return this.balance;
    }

    pay(amountToPay: number) : void{
        if(this.canPay(amountToPay)) {
            console.log(`Paid ${amountToPay} using ${this.constructor.name}`)
            this.balance -= amountToPay;
        } else if(this.successor) {
            console.log(`Cannot pay using ${this.constructor.name}, proceeding...`)
            this.successor.pay(amountToPay)
        } else {
            throw new Error('None of the accounts have enough balance')
        }
    }
}

class Bank extends Account {
    protected balance

    constructor(balance: number) {
        super()
        this.balance = balance;
    }
}

class Paypal extends Account {
    protected balance

    constructor(balance: number) {
        super()
        this.balance = balance;
    }
}

class Bitcoin extends Account {
    protected balance

    constructor(balance: number) {
        super()
        this.balance = balance;
    }
}

Now let's prepare the chain using the links defined above (i.e. Bank, Paypal, Bitcoin)

// Let's prepare a chain like below
//      $bank->$paypal->$bitcoin
//
// First priority bank
//      If bank can't pay then paypal
//      If paypal can't pay then bit coin
test('test chain-of-responsibility design patterns', () => {
    const bank = new Bank(100)
    const paypal = new Paypal(200)
    const bitcoin = new Bitcoin(300)
    bank.setNext(paypal)
    paypal.setNext(bitcoin)

    expect(bank.getBalance()).toEqual(100)
    expect(paypal.getBalance()).toEqual(200)
    expect(bitcoin.getBalance()).toEqual(300)

    // Let's try to pay using the first priority i.e. bank
    bank.pay(259)

    // Output will be
    // ==============
    // Cannot pay using bank. Proceeding ..
    // Cannot pay using paypal. Proceeding ..:
    // Paid 259 using Bitcoin!
    expect(bank.getBalance()).toEqual(100)
    expect(paypal.getBalance()).toEqual(200)
    expect(bitcoin.getBalance()).toEqual(41)
})

References