Checks in a line, each free to stop the request: how to give every check its own object, what a chain must decide, and why the order is part of the behaviour.
Before a payment is charged, three questions are asked in turn. Is the amount within the limit? Does it look like fraud? Does the customer have the money? Any of the three can say stop, and the later ones should never run once an earlier one has. Most people write that as one function with an if for each question. It works, until the questions multiply, change order or differ per customer.
That is the Chain of Responsibility pattern: give each check its own object, link the objects in a line, and send the request to the first one. Each either stops the request with an answer or passes it to the next. The sender knows only the first link, and never which one finally answered. You have already met it in the pub-sub logger, where a record passes through a level filter, a redactor and a publisher, and that round is the only one that names it. This lesson builds the chain from scratch, and says plainly what the logger's chain does differently.
The lesson builds the payment check three times: as an if-ladder, as a classic chain of handlers, and as middleware, where each link wraps the rest.
An expense claim handed to your team lead is Chain of Responsibility: each person approves it or passes it up, and you know only the first.
The first version: every check in one function#
The first version puts the checks in one function, in a fixed order, with a flag that was added when trusted customers were allowed to skip the fraud check:
# pay_v1.py: every check in one function, in a fixed order
class Bank:
"""A stand-in for the bank: it only knows balances."""
def __init__(self, balances: dict) -> None:
self.balances = balances
def balance(self, user: str) -> int:
return self.balances[user]
def pay(user: str, amount: int, age_days: int, bank: Bank, trusted: bool = False) -> str:
if amount > 20000:
return "declined (over the limit of 20000)"
if not trusted and age_days < 7 and amount > 5000: # trusted customers skip the fraud check
return "held (new account, over 5000)"
if bank.balance(user) >= amount:
return "approved (charged)"
return "declined (insufficient funds)"
bank = Bank({"asha": 5000, "ravi": 90000, "meena": 9000, "kiran": 100, "tara": 40000, "dev": 9000})
# user, amount, days since the account opened, trusted
for user, amount, age, trusted in [
("asha", 450, 400, False),
("ravi", 25000, 400, False),
("meena", 6000, 2, False),
("kiran", 450, 400, False),
("tara", 25000, 2, False),
("dev", 6000, 2, True),
]:
print(f"{user} {amount}: {pay(user, amount, age, bank, trusted)}")// PayV1.java: every check in one function, in a fixed order
import java.util.Map;
class PayV1 {
// A stand-in for the bank: it only knows balances.
static class Bank {
final Map<String, Integer> balances;
Bank(Map<String, Integer> balances) { this.balances = balances; }
int balance(String user) { return balances.get(user); }
}
static String pay(String user, int amount, int ageDays, Bank bank, boolean trusted) {
if (amount > 20000) {
return "declined (over the limit of 20000)";
}
if (!trusted && ageDays < 7 && amount > 5000) { // trusted customers skip the fraud check
return "held (new account, over 5000)";
}
if (bank.balance(user) >= amount) {
return "approved (charged)";
}
return "declined (insufficient funds)";
}
public static void main(String[] args) {
Bank bank = new Bank(Map.of("asha", 5000, "ravi", 90000, "meena", 9000, "kiran", 100, "tara", 40000, "dev", 9000));
// user, amount, days since the account opened, trusted
Object[][] requests = {
{"asha", 450, 400, false},
{"ravi", 25000, 400, false},
{"meena", 6000, 2, false},
{"kiran", 450, 400, false},
{"tara", 25000, 2, false},
{"dev", 6000, 2, true},
};
for (Object[] r : requests) {
String user = (String) r[0];
int amount = (int) r[1];
System.out.println(user + " " + amount + ": " + pay(user, amount, (int) r[2], bank, (boolean) r[3]));
}
}
}// pay_v1.cpp: every check in one function, in a fixed order
#include <iostream>
#include <map>
#include <string>
#include <vector>
// A stand-in for the bank: it only knows balances.
struct Bank {
std::map<std::string, int> balances;
int balance(const std::string& user) const { return balances.at(user); }
};
std::string pay(const std::string& user, int amount, int ageDays, const Bank& bank, bool trusted) {
if (amount > 20000) {
return "declined (over the limit of 20000)";
}
if (!trusted && ageDays < 7 && amount > 5000) { // trusted customers skip the fraud check
return "held (new account, over 5000)";
}
if (bank.balance(user) >= amount) {
return "approved (charged)";
}
return "declined (insufficient funds)";
}
struct Case {
std::string user;
int amount;
int ageDays; // days since the account opened
bool trusted;
};
int main() {
const Bank bank{{{"asha", 5000}, {"ravi", 90000}, {"meena", 9000}, {"kiran", 100}, {"tara", 40000}, {"dev", 9000}}};
const std::vector<Case> cases = {
{"asha", 450, 400, false},
{"ravi", 25000, 400, false},
{"meena", 6000, 2, false},
{"kiran", 450, 400, false},
{"tara", 25000, 2, false},
{"dev", 6000, 2, true},
};
for (const Case& c : cases) {
std::cout << c.user << " " << c.amount << ": " << pay(c.user, c.amount, c.ageDays, bank, c.trusted) << "\n";
}
}// payV1.js: every check in one function, in a fixed order
class Bank {
// A stand-in for the bank: it only knows balances.
constructor(balances) {
this.balances = balances;
}
balance(user) {
return this.balances[user];
}
}
function pay(user, amount, ageDays, bank, trusted = false) {
if (amount > 20000) {
return "declined (over the limit of 20000)";
}
if (!trusted && ageDays < 7 && amount > 5000) {
// trusted customers skip the fraud check
return "held (new account, over 5000)";
}
if (bank.balance(user) >= amount) {
return "approved (charged)";
}
return "declined (insufficient funds)";
}
const bank = new Bank({ asha: 5000, ravi: 90000, meena: 9000, kiran: 100, tara: 40000, dev: 9000 });
// user, amount, days since the account opened, trusted
for (const [user, amount, age, trusted] of [
["asha", 450, 400, false],
["ravi", 25000, 400, false],
["meena", 6000, 2, false],
["kiran", 450, 400, false],
["tara", 25000, 2, false],
["dev", 6000, 2, true],
]) {
console.log(`${user} ${amount}: ${pay(user, amount, age, bank, trusted)}`);
}asha 450: approved (charged)
ravi 25000: declined (over the limit of 20000)
meena 6000: held (new account, over 5000)
kiran 450: declined (insufficient funds)
tara 25000: declined (over the limit of 20000)
dev 6000: approved (charged)It runs, and for three checks the code is honest and small. Nothing is wrong yet. The trouble is what the next six months do to it:
- The order is the position in the function. To put a new check between the fraud check and the bank, you edit the middle of a function that everything else depends on.
- Exceptions become parameters.
trustedis already a flag inside the fraud rule. The next exception (a merchant, a country, a test account) adds another parameter to every caller. - One chain for everyone. Giving trusted customers a different sequence means more flags, not a different sequence.
- No check can be tested alone. To test the fraud rule you must call
pay, and so supply a bank. - Two teams, one function. Risk owns the fraud rule, payments own the bank call, and both edit the same lines.
Where does a new check go?
Compliance asks for a blocked country check that must run after the limit check and before the fraud check. In the first version, what do you edit, and what do you risk?
One strong answer
You edit the middle of pay, and you add a country parameter that every caller must now pass. The new check sits between two others, so it can disturb both: a wrong return order changes what existing customers are told. Nothing about the new rule can be tested without the limit rule and the bank around it. In a chain, the new check is a new class, and you insert it with one then() call.
The pattern#
The book that named the pattern (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states its intent in two sentences: "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."
In the payment check that gives four roles:
| Role | In the payment check | Its one job |
|---|---|---|
| Handler (the interface) | Handler with handle(request) and check(request) |
Hold the next handler, and either stop the request or pass it on |
| Concrete handlers | LimitCheck, FraudCheck, BankCharge |
Each answers one question: is this request mine to stop? |
| Request | Request (user, amount, account age) |
What travels along the chain; it also keeps a trail of the handlers that looked at it |
| Client | the code that builds the chain | Decide the order, and send the request to the first handler |
%% title: Chain of Responsibility: the payment check
classDiagram
class Handler {
<<abstract>>
-next Handler
+then(next) Handler
+handle(req) Verdict
#check(req) Verdict
}
class LimitCheck
class FraudCheck
class BankCharge
class Request {
+user str
+amount int
+age_days int
+trail list
}
class Verdict {
+status str
+reason str
}
class Bank {
+balance(user) int
}
Handler --> Handler : next
Handler <|-- LimitCheck
Handler <|-- FraudCheck
Handler <|-- BankCharge
Handler ..> Request : reads
Handler ..> Verdict : returns
BankCharge --> Bank : asksRead the arrows. The arrow from Handler back to itself, labelled next, is the chain: each handler holds the next one, and nobody holds the whole list. The sender talks to the first handler and never learns how many there are. The three triangles are inheritance, as in Template Method, and handle is in fact a template method: it notes the handler on the trail, asks check, and then stops or passes on. A concrete handler writes only check, so no handler can forget to pass the request on.
The code#
The base class owns the passing on. Each handler answers one question, returning a verdict to stop the request or nothing to pass it on:
# chain.py: Chain of Responsibility: each handler may stop the request or pass it on
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Request:
user: str
amount: int # rupees
age_days: int # days since the account opened
trail: list = field(default_factory=list) # the handlers that looked at it
@dataclass(frozen=True)
class Verdict:
status: str # approved, declined, held or unhandled
reason: str
class Bank:
"""A stand-in for the bank: it only knows balances."""
def __init__(self, balances: dict) -> None:
self.balances = balances
def balance(self, user: str) -> int:
return self.balances[user]
class Handler(ABC):
def __init__(self) -> None:
self.next: Optional["Handler"] = None
def then(self, nxt: "Handler") -> "Handler": # link the next handler and return it, so calls can be chained
self.next = nxt
return nxt
def handle(self, req: Request) -> Verdict: # a verdict stops the request, None passes it on
req.trail.append(type(self).__name__)
verdict = self.check(req)
if verdict is not None:
return verdict
if self.next is None:
return Verdict("unhandled", "nobody handled it")
return self.next.handle(req)
@abstractmethod
def check(self, req: Request) -> Optional[Verdict]: ...
class LimitCheck(Handler):
def __init__(self, max_amount: int) -> None:
super().__init__()
self.max_amount = max_amount
def check(self, req: Request) -> Optional[Verdict]:
if req.amount > self.max_amount:
return Verdict("declined", f"over the limit of {self.max_amount}")
return None
class FraudCheck(Handler):
def __init__(self, new_days: int, big: int) -> None:
super().__init__()
self.new_days, self.big, self.calls = new_days, big, 0
def check(self, req: Request) -> Optional[Verdict]:
self.calls += 1
if req.age_days < self.new_days and req.amount > self.big:
return Verdict("held", f"new account, over {self.big}")
return None
class BankCharge(Handler): # the end of the chain: it always answers
def __init__(self, bank: Bank) -> None:
super().__init__()
self.bank = bank
def check(self, req: Request) -> Optional[Verdict]:
if self.bank.balance(req.user) >= req.amount:
return Verdict("approved", "charged")
return Verdict("declined", "insufficient funds")
bank = Bank({"asha": 5000, "ravi": 90000, "meena": 9000, "kiran": 100, "tara": 40000, "dev": 9000})
REQUESTS = [("asha", 450, 400), ("ravi", 25000, 400), ("meena", 6000, 2), ("kiran", 450, 400), ("tara", 25000, 2)]
def run(head: Handler, requests: list) -> list:
return [(u, a, head.handle(Request(u, a, age))) for u, a, age in requests]
def show(user: str, amount: int, verdict: Verdict, trail: list) -> None:
print(f"{user} {amount}: {verdict.status} ({verdict.reason}) via {' > '.join(trail)}")
print("-- limit, fraud, bank")
limit, fraud = LimitCheck(20000), FraudCheck(7, 5000)
limit.then(fraud).then(BankCharge(bank))
first = []
for user, amount, age in REQUESTS:
req = Request(user, amount, age)
verdict = limit.handle(req)
first.append(verdict)
show(user, amount, verdict, req.trail)
print(f"fraud checks run: {fraud.calls}")
print("-- trusted customers get a chain without the fraud check")
trusted = LimitCheck(20000)
trusted.then(BankCharge(bank))
req = Request("dev", 6000, 2)
show("dev", 6000, trusted.handle(req), req.trail)
print("-- the same checks, fraud first")
fraud2 = FraudCheck(7, 5000)
fraud2.then(LimitCheck(20000)).then(BankCharge(bank))
for (user, amount, _), before, (_, _, after) in zip(REQUESTS, first, run(fraud2, REQUESTS)):
if before.status != after.status:
print(f"{user} {amount}: {before.status} -> {after.status}")
print(f"fraud checks run: {fraud2.calls}")
print("-- a chain with no bank at the end")
short = LimitCheck(20000)
short.then(FraudCheck(7, 5000))
req = Request("asha", 450, 400)
show("asha", 450, short.handle(req), req.trail)// Chain.java: Chain of Responsibility: each handler may stop the request or pass it on
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class ChainDemo {
static class Request {
final String user;
final int amount; // rupees
final int ageDays; // days since the account opened
final List<String> trail = new ArrayList<>(); // the handlers that looked at it
Request(String user, int amount, int ageDays) {
this.user = user;
this.amount = amount;
this.ageDays = ageDays;
}
}
record Verdict(String status, String reason) {} // approved, declined, held or unhandled
// A stand-in for the bank: it only knows balances.
static class Bank {
final Map<String, Integer> balances;
Bank(Map<String, Integer> balances) { this.balances = balances; }
int balance(String user) { return balances.get(user); }
}
static abstract class Handler {
Handler next;
Handler then(Handler nxt) { // link the next handler and return it, so calls can be chained
this.next = nxt;
return nxt;
}
final Verdict handle(Request req) { // a verdict stops the request, null passes it on
req.trail.add(getClass().getSimpleName());
Verdict verdict = check(req);
if (verdict != null) {
return verdict;
}
if (next == null) {
return new Verdict("unhandled", "nobody handled it");
}
return next.handle(req);
}
abstract Verdict check(Request req);
}
static class LimitCheck extends Handler {
final int maxAmount;
LimitCheck(int maxAmount) { this.maxAmount = maxAmount; }
Verdict check(Request req) {
if (req.amount > maxAmount) {
return new Verdict("declined", "over the limit of " + maxAmount);
}
return null;
}
}
static class FraudCheck extends Handler {
final int newDays;
final int big;
int calls = 0;
FraudCheck(int newDays, int big) { this.newDays = newDays; this.big = big; }
Verdict check(Request req) {
calls++;
if (req.ageDays < newDays && req.amount > big) {
return new Verdict("held", "new account, over " + big);
}
return null;
}
}
static class BankCharge extends Handler { // the end of the chain: it always answers
final Bank bank;
BankCharge(Bank bank) { this.bank = bank; }
Verdict check(Request req) {
if (bank.balance(req.user) >= req.amount) {
return new Verdict("approved", "charged");
}
return new Verdict("declined", "insufficient funds");
}
}
static final Object[][] REQUESTS = {
{"asha", 450, 400}, {"ravi", 25000, 400}, {"meena", 6000, 2}, {"kiran", 450, 400}, {"tara", 25000, 2},
};
static void show(String user, int amount, Verdict v, List<String> trail) {
System.out.println(user + " " + amount + ": " + v.status() + " (" + v.reason() + ") via " + String.join(" > ", trail));
}
public static void main(String[] args) {
Bank bank = new Bank(Map.of("asha", 5000, "ravi", 90000, "meena", 9000, "kiran", 100, "tara", 40000, "dev", 9000));
System.out.println("-- limit, fraud, bank");
LimitCheck limit = new LimitCheck(20000);
FraudCheck fraud = new FraudCheck(7, 5000);
limit.then(fraud).then(new BankCharge(bank));
List<Verdict> first = new ArrayList<>();
for (Object[] r : REQUESTS) {
Request req = new Request((String) r[0], (int) r[1], (int) r[2]);
Verdict v = limit.handle(req);
first.add(v);
show(req.user, req.amount, v, req.trail);
}
System.out.println("fraud checks run: " + fraud.calls);
System.out.println("-- trusted customers get a chain without the fraud check");
LimitCheck trusted = new LimitCheck(20000);
trusted.then(new BankCharge(bank));
Request dev = new Request("dev", 6000, 2);
show("dev", 6000, trusted.handle(dev), dev.trail);
System.out.println("-- the same checks, fraud first");
FraudCheck fraud2 = new FraudCheck(7, 5000);
fraud2.then(new LimitCheck(20000)).then(new BankCharge(bank));
for (int i = 0; i < REQUESTS.length; i++) {
Object[] r = REQUESTS[i];
Verdict after = fraud2.handle(new Request((String) r[0], (int) r[1], (int) r[2]));
if (!first.get(i).status().equals(after.status())) {
System.out.println(r[0] + " " + r[1] + ": " + first.get(i).status() + " -> " + after.status());
}
}
System.out.println("fraud checks run: " + fraud2.calls);
System.out.println("-- a chain with no bank at the end");
LimitCheck shortChain = new LimitCheck(20000);
shortChain.then(new FraudCheck(7, 5000));
Request asha = new Request("asha", 450, 400);
show("asha", 450, shortChain.handle(asha), asha.trail);
}
}// chain.cpp: Chain of Responsibility: each handler may stop the request or pass it on
#include <iostream>
#include <map>
#include <optional>
#include <string>
#include <vector>
struct Request {
std::string user;
int amount; // rupees
int ageDays; // days since the account opened
std::vector<std::string> trail; // the handlers that looked at it
};
struct Verdict {
std::string status; // approved, declined, held or unhandled
std::string reason;
};
// A stand-in for the bank: it only knows balances.
struct Bank {
std::map<std::string, int> balances;
int balance(const std::string& user) const { return balances.at(user); }
};
class Handler {
public:
virtual ~Handler() = default;
Handler& then(Handler& next) { // link the next handler and return it, so calls can be chained
next_ = &next;
return next;
}
Verdict handle(Request& req) const { // a verdict stops the request, an empty answer passes it on
req.trail.push_back(name());
std::optional<Verdict> verdict = check(req);
if (verdict) return *verdict;
if (next_ == nullptr) return {"unhandled", "nobody handled it"};
return next_->handle(req);
}
protected:
virtual std::string name() const = 0;
virtual std::optional<Verdict> check(const Request& req) const = 0;
private:
Handler* next_ = nullptr;
};
class LimitCheck : public Handler {
public:
explicit LimitCheck(int maxAmount) : maxAmount_(maxAmount) {}
protected:
std::string name() const override { return "LimitCheck"; }
std::optional<Verdict> check(const Request& req) const override {
if (req.amount > maxAmount_) return Verdict{"declined", "over the limit of " + std::to_string(maxAmount_)};
return std::nullopt;
}
private:
int maxAmount_;
};
class FraudCheck : public Handler {
public:
FraudCheck(int newDays, int big) : newDays_(newDays), big_(big) {}
int calls() const { return calls_; }
protected:
std::string name() const override { return "FraudCheck"; }
std::optional<Verdict> check(const Request& req) const override {
calls_++;
if (req.ageDays < newDays_ && req.amount > big_) return Verdict{"held", "new account, over " + std::to_string(big_)};
return std::nullopt;
}
private:
int newDays_;
int big_;
mutable int calls_ = 0;
};
class BankCharge : public Handler { // the end of the chain: it always answers
public:
explicit BankCharge(const Bank& bank) : bank_(bank) {}
protected:
std::string name() const override { return "BankCharge"; }
std::optional<Verdict> check(const Request& req) const override {
if (bank_.balance(req.user) >= req.amount) return Verdict{"approved", "charged"};
return Verdict{"declined", "insufficient funds"};
}
private:
const Bank& bank_;
};
struct Case {
std::string user;
int amount;
int ageDays;
};
std::string joinTrail(const std::vector<std::string>& trail) {
std::string out;
for (size_t i = 0; i < trail.size(); i++) out += (i ? " > " : "") + trail[i];
return out;
}
void show(const std::string& user, int amount, const Verdict& v, const std::vector<std::string>& trail) {
std::cout << user << " " << amount << ": " << v.status << " (" << v.reason << ") via " << joinTrail(trail) << "\n";
}
int main() {
const Bank bank{{{"asha", 5000}, {"ravi", 90000}, {"meena", 9000}, {"kiran", 100}, {"tara", 40000}, {"dev", 9000}}};
const std::vector<Case> requests = {{"asha", 450, 400}, {"ravi", 25000, 400}, {"meena", 6000, 2}, {"kiran", 450, 400}, {"tara", 25000, 2}};
std::cout << "-- limit, fraud, bank\n";
LimitCheck limit(20000);
FraudCheck fraud(7, 5000);
BankCharge charge(bank);
limit.then(fraud).then(charge);
std::vector<Verdict> first;
for (const Case& c : requests) {
Request req{c.user, c.amount, c.ageDays, {}};
Verdict v = limit.handle(req);
first.push_back(v);
show(c.user, c.amount, v, req.trail);
}
std::cout << "fraud checks run: " << fraud.calls() << "\n";
std::cout << "-- trusted customers get a chain without the fraud check\n";
LimitCheck trustedLimit(20000);
BankCharge trustedCharge(bank);
trustedLimit.then(trustedCharge);
Request dev{"dev", 6000, 2, {}};
show("dev", 6000, trustedLimit.handle(dev), dev.trail);
std::cout << "-- the same checks, fraud first\n";
FraudCheck fraud2(7, 5000);
LimitCheck limit2(20000);
BankCharge charge2(bank);
fraud2.then(limit2).then(charge2);
for (size_t i = 0; i < requests.size(); i++) {
Request req{requests[i].user, requests[i].amount, requests[i].ageDays, {}};
Verdict after = fraud2.handle(req);
if (first[i].status != after.status) {
std::cout << requests[i].user << " " << requests[i].amount << ": " << first[i].status << " -> " << after.status << "\n";
}
}
std::cout << "fraud checks run: " << fraud2.calls() << "\n";
std::cout << "-- a chain with no bank at the end\n";
LimitCheck shortLimit(20000);
FraudCheck shortFraud(7, 5000);
shortLimit.then(shortFraud);
Request asha{"asha", 450, 400, {}};
show("asha", 450, shortLimit.handle(asha), asha.trail);
}// chain.js: Chain of Responsibility: each handler may stop the request or pass it on
class Request {
constructor(user, amount, ageDays) {
this.user = user;
this.amount = amount; // rupees
this.ageDays = ageDays; // days since the account opened
this.trail = []; // the handlers that looked at it
}
}
const verdict = (status, reason) => ({ status, reason }); // approved, declined, held or unhandled
class Bank {
// A stand-in for the bank: it only knows balances.
constructor(balances) {
this.balances = balances;
}
balance(user) {
return this.balances[user];
}
}
class Handler {
constructor() {
this.next = null;
}
then(next) {
// link the next handler and return it, so calls can be chained
this.next = next;
return next;
}
handle(req) {
// a verdict stops the request, null passes it on
req.trail.push(this.constructor.name);
const v = this.check(req);
if (v !== null) {
return v;
}
if (this.next === null) {
return verdict("unhandled", "nobody handled it");
}
return this.next.handle(req);
}
check(req) {
throw new Error("a handler must define check");
}
}
class LimitCheck extends Handler {
constructor(maxAmount) {
super();
this.maxAmount = maxAmount;
}
check(req) {
if (req.amount > this.maxAmount) {
return verdict("declined", `over the limit of ${this.maxAmount}`);
}
return null;
}
}
class FraudCheck extends Handler {
constructor(newDays, big) {
super();
this.newDays = newDays;
this.big = big;
this.calls = 0;
}
check(req) {
this.calls += 1;
if (req.ageDays < this.newDays && req.amount > this.big) {
return verdict("held", `new account, over ${this.big}`);
}
return null;
}
}
class BankCharge extends Handler {
// the end of the chain: it always answers
constructor(bank) {
super();
this.bank = bank;
}
check(req) {
if (this.bank.balance(req.user) >= req.amount) {
return verdict("approved", "charged");
}
return verdict("declined", "insufficient funds");
}
}
const bank = new Bank({ asha: 5000, ravi: 90000, meena: 9000, kiran: 100, tara: 40000, dev: 9000 });
const REQUESTS = [["asha", 450, 400], ["ravi", 25000, 400], ["meena", 6000, 2], ["kiran", 450, 400], ["tara", 25000, 2]];
const run = (head, requests) => requests.map(([u, a, age]) => [u, a, head.handle(new Request(u, a, age))]);
function show(user, amount, v, trail) {
console.log(`${user} ${amount}: ${v.status} (${v.reason}) via ${trail.join(" > ")}`);
}
console.log("-- limit, fraud, bank");
const limit = new LimitCheck(20000);
const fraud = new FraudCheck(7, 5000);
limit.then(fraud).then(new BankCharge(bank));
const first = [];
for (const [user, amount, age] of REQUESTS) {
const req = new Request(user, amount, age);
const v = limit.handle(req);
first.push(v);
show(user, amount, v, req.trail);
}
console.log(`fraud checks run: ${fraud.calls}`);
console.log("-- trusted customers get a chain without the fraud check");
const trusted = new LimitCheck(20000);
trusted.then(new BankCharge(bank));
let req = new Request("dev", 6000, 2);
show("dev", 6000, trusted.handle(req), req.trail);
console.log("-- the same checks, fraud first");
const fraud2 = new FraudCheck(7, 5000);
fraud2.then(new LimitCheck(20000)).then(new BankCharge(bank));
run(fraud2, REQUESTS).forEach(([user, amount, after], i) => {
if (first[i].status !== after.status) {
console.log(`${user} ${amount}: ${first[i].status} -> ${after.status}`);
}
});
console.log(`fraud checks run: ${fraud2.calls}`);
console.log("-- a chain with no bank at the end");
const short = new LimitCheck(20000);
short.then(new FraudCheck(7, 5000));
req = new Request("asha", 450, 400);
show("asha", 450, short.handle(req), req.trail);-- limit, fraud, bank
asha 450: approved (charged) via LimitCheck > FraudCheck > BankCharge
ravi 25000: declined (over the limit of 20000) via LimitCheck
meena 6000: held (new account, over 5000) via LimitCheck > FraudCheck
kiran 450: declined (insufficient funds) via LimitCheck > FraudCheck > BankCharge
tara 25000: declined (over the limit of 20000) via LimitCheck
fraud checks run: 3
-- trusted customers get a chain without the fraud check
dev 6000: approved (charged) via LimitCheck > BankCharge
-- the same checks, fraud first
tara 25000: declined -> held
fraud checks run: 5
-- a chain with no bank at the end
asha 450: unhandled (nobody handled it) via LimitCheck > FraudCheckCheck by hand. The limit is 20000, and a new account is under 7 days old. Ravi's 25000 is over the limit, so LimitCheck answers and nobody else looks (the trail has one name). Meena's 6000 passes the limit, but her account is 2 days old and 6000 is over 5000, so FraudCheck holds it. Kiran's 450 passes both, and the bank finds a balance of 100, so BankCharge declines it: all three handlers looked. The fraud handler counted 3 calls, because Ravi's and Tara's payments never reached it. The trusted chain simply leaves the fraud check out, and not a line of the existing handlers changed.
What each language brings to the same design:
- Python: an
ABCwhosehandleis concrete and whosecheckis abstract; a subclass that forgetscheckcan't be instantiated. The handler's name for the trail comes fromtype(self).__name__. - Java:
handleisfinal, so no handler can break the passing on; the name comes fromgetClass().getSimpleName(). - C++:
handleis a non-virtual function calling the virtualcheck. The chain holds plain pointers to handlers that live inmain, so each handler's lifetime is the caller's job. C++ has no portable, readable class name at run time (typeid(*this).name()gives a compiler-specific string), so each handler says its own withname(). - JavaScript: classes with a
checkthat throws if it isn't overridden. The trail name comes fromthis.constructor.name.
Kiran's payment passes the limit and the fraud check, and the bank, the last handler, declines it: all three looked, and the answer returns to the client.
Three questions every chain must answer#
A chain looks finished when the handlers work. It isn't, until you can answer three questions.
1. What if nobody answers? The last line of the output is a chain with no bank at the end: Asha's ordinary 450 payment passes the limit check, passes the fraud check, falls off the end, and comes back as unhandled. That is the honest answer for the chain, and the lesson's code returns it on purpose. The book warns about exactly this: with no explicit receiver, receipt isn't guaranteed. Make the end of the chain a decision: an always-answering last handler (as BankCharge is), or a default verdict. For a payment, no answer must never mean approved; fail closed.
2. Who decides the order? The client does, and the order is part of the behaviour, not a detail. The same five payments, with the fraud check first, give one different answer: Tara's 25000 on a 2-day-old account was declined (over the limit) and is now held (new account). The fraud handler also runs 5 times instead of 3, because the limit check no longer shields it. If the fraud check were a call to another service, that would be two extra network calls. A good rule is to put cheap and decisive checks first, and to decide the order on purpose and test it, because moving two handlers can change what customers are told.
3. Who builds the chain? Not the handlers, which only know their own next. A client, a factory or a config builds it, and that means chains are data. The trusted chain above is limit.then(bank): a different sequence for different customers, with no flag in any handler. In the first version that took the trusted flag; here it takes one more line in the code that builds the chain.
Cheap first
The limit check is one comparison. The fraud check calls a scoring service over the network. Which goes first, and what do you have to be careful about when you reorder?
One strong answer
The limit check, because it is cheap and often decisive: every payment it declines never pays for the network call. In the lesson's run, putting it first saves 2 of 5 fraud checks. The care needed: if a request can fail two checks, the order picks which answer the customer gets (declined for the limit, or held for review), so changing the order changes the product's behaviour. Decide the order on purpose, say why in a comment, and keep a test for the one case where two checks disagree.
The same five payments in two orders: with the limit check first the fraud check runs 3 times, second it runs 5 times, and one answer changes.
A chain does not have to be linked objects. If the checks are all known when the chain is built, a plain list does the same job with no next pointers:
for check in checks:
verdict = check(request)
if verdict is not None:
return verdict
return Verdict("unhandled", "nobody handled it")It is the same pattern, with the loop in the client instead of in each handler. Prefer the list when the order is fixed at build time; prefer linked handlers when the chain is assembled in pieces, or when a handler needs to decide at run time who comes next.
Middleware: a link that wraps the rest#
There is a second common shape, and it is the one web frameworks use. Instead of returning "mine" or "not mine", each link is a function that receives the request and a next function, and decides whether to call it. A link can do work before calling next, and work after, with the answer in hand:
# middleware.py: each link gets the request and "next", and decides whether to call it
from dataclasses import dataclass
@dataclass
class Request:
user: str
amount: int # rupees
age_days: int # days since the account opened
@dataclass(frozen=True)
class Verdict:
status: str # approved, declined or held
reason: str
class Bank:
"""A stand-in for the bank: it only knows balances."""
def __init__(self, balances: dict) -> None:
self.balances = balances
def balance(self, user: str) -> int:
return self.balances[user]
def audit(log: list): # runs before and after the rest of the chain
def link(req, nxt):
log.append(f"in {req.user} {req.amount}")
verdict = nxt(req)
log.append(f"out {req.user} {verdict.status}")
return verdict
return link
def limit(max_amount: int):
def link(req, nxt):
if req.amount > max_amount:
return Verdict("declined", f"over the limit of {max_amount}") # stops here: nxt is never called
return nxt(req)
return link
def fraud(new_days: int, big: int):
def link(req, nxt):
if req.age_days < new_days and req.amount > big:
return Verdict("held", f"new account, over {big}")
return nxt(req)
return link
def bank_charge(bank: Bank): # the last link: no next
def end(req):
if bank.balance(req.user) >= req.amount:
return Verdict("approved", "charged")
return Verdict("declined", "insufficient funds")
return end
def build(links: list, end): # fold the links from the end, so the first link runs first
handler = end
for link in reversed(links):
handler = (lambda l, n: lambda req: l(req, n))(link, handler)
return handler
bank = Bank({"asha": 5000, "ravi": 90000, "meena": 9000})
REQUESTS = [Request("asha", 450, 400), Request("meena", 6000, 2), Request("ravi", 25000, 400)]
log = []
pay = build([audit(log), limit(20000), fraud(7, 5000)], bank_charge(bank))
for req in REQUESTS:
verdict = pay(req)
print(f"{req.user} {req.amount}: {verdict.status} ({verdict.reason})")
print("audit log:")
for line in log:
print(line)
log2 = []
late = build([limit(20000), audit(log2), fraud(7, 5000)], bank_charge(bank))
for req in REQUESTS:
late(req)
print(f"audit placed after the limit saw {len(log2) // 2} of {len(REQUESTS)} requests")// Middleware.java: each link gets the request and "next", and decides whether to call it
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
class MiddlewareDemo {
record Request(String user, int amount, int ageDays) {} // amount in rupees; age in days since the account opened
record Verdict(String status, String reason) {} // approved, declined or held
// A stand-in for the bank: it only knows balances.
static class Bank {
final Map<String, Integer> balances;
Bank(Map<String, Integer> balances) { this.balances = balances; }
int balance(String user) { return balances.get(user); }
}
interface Next { Verdict call(Request req); }
interface Link { Verdict handle(Request req, Next next); }
static Link audit(List<String> log) { // runs before and after the rest of the chain
return (req, next) -> {
log.add("in " + req.user() + " " + req.amount());
Verdict v = next.call(req);
log.add("out " + req.user() + " " + v.status());
return v;
};
}
static Link limit(int maxAmount) {
return (req, next) -> {
if (req.amount() > maxAmount) {
return new Verdict("declined", "over the limit of " + maxAmount); // stops here: next is never called
}
return next.call(req);
};
}
static Link fraud(int newDays, int big) {
return (req, next) -> {
if (req.ageDays() < newDays && req.amount() > big) {
return new Verdict("held", "new account, over " + big);
}
return next.call(req);
};
}
static Next bankCharge(Bank bank) { // the last link: no next
return req -> {
if (bank.balance(req.user()) >= req.amount()) {
return new Verdict("approved", "charged");
}
return new Verdict("declined", "insufficient funds");
};
}
static Next build(List<Link> links, Next end) { // fold the links from the end, so the first link runs first
Next handler = end;
for (int i = links.size() - 1; i >= 0; i--) {
Link link = links.get(i);
Next following = handler;
handler = req -> link.handle(req, following);
}
return handler;
}
public static void main(String[] args) {
Bank bank = new Bank(Map.of("asha", 5000, "ravi", 90000, "meena", 9000));
List<Request> requests = List.of(new Request("asha", 450, 400), new Request("meena", 6000, 2), new Request("ravi", 25000, 400));
List<String> log = new ArrayList<>();
Next pay = build(List.of(audit(log), limit(20000), fraud(7, 5000)), bankCharge(bank));
for (Request req : requests) {
Verdict v = pay.call(req);
System.out.println(req.user() + " " + req.amount() + ": " + v.status() + " (" + v.reason() + ")");
}
System.out.println("audit log:");
for (String line : log) {
System.out.println(line);
}
List<String> log2 = new ArrayList<>();
Next late = build(List.of(limit(20000), audit(log2), fraud(7, 5000)), bankCharge(bank));
for (Request req : requests) {
late.call(req);
}
System.out.println("audit placed after the limit saw " + log2.size() / 2 + " of " + requests.size() + " requests");
}
}// middleware.cpp: each link gets the request and "next", and decides whether to call it
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>
struct Request {
std::string user;
int amount; // rupees
int ageDays; // days since the account opened
};
struct Verdict {
std::string status; // approved, declined or held
std::string reason;
};
// A stand-in for the bank: it only knows balances.
struct Bank {
std::map<std::string, int> balances;
int balance(const std::string& user) const { return balances.at(user); }
};
using Next = std::function<Verdict(const Request&)>;
using Link = std::function<Verdict(const Request&, const Next&)>;
Link audit(std::vector<std::string>& log) { // runs before and after the rest of the chain
return [&log](const Request& req, const Next& next) {
log.push_back("in " + req.user + " " + std::to_string(req.amount));
Verdict v = next(req);
log.push_back("out " + req.user + " " + v.status);
return v;
};
}
Link limit(int maxAmount) {
return [maxAmount](const Request& req, const Next& next) {
if (req.amount > maxAmount) {
return Verdict{"declined", "over the limit of " + std::to_string(maxAmount)}; // stops here: next is never called
}
return next(req);
};
}
Link fraud(int newDays, int big) {
return [newDays, big](const Request& req, const Next& next) {
if (req.ageDays < newDays && req.amount > big) {
return Verdict{"held", "new account, over " + std::to_string(big)};
}
return next(req);
};
}
Next bankCharge(const Bank& bank) { // the last link: no next
return [&bank](const Request& req) {
if (bank.balance(req.user) >= req.amount) {
return Verdict{"approved", "charged"};
}
return Verdict{"declined", "insufficient funds"};
};
}
Next build(const std::vector<Link>& links, Next end) { // fold the links from the end, so the first link runs first
Next handler = end;
for (auto it = links.rbegin(); it != links.rend(); ++it) {
Link link = *it;
Next following = handler;
handler = [link, following](const Request& req) { return link(req, following); };
}
return handler;
}
int main() {
const Bank bank{{{"asha", 5000}, {"ravi", 90000}, {"meena", 9000}}};
const std::vector<Request> requests = {{"asha", 450, 400}, {"meena", 6000, 2}, {"ravi", 25000, 400}};
std::vector<std::string> log;
Next pay = build({audit(log), limit(20000), fraud(7, 5000)}, bankCharge(bank));
for (const Request& req : requests) {
Verdict v = pay(req);
std::cout << req.user << " " << req.amount << ": " << v.status << " (" << v.reason << ")\n";
}
std::cout << "audit log:\n";
for (const std::string& line : log) std::cout << line << "\n";
std::vector<std::string> log2;
Next late = build({limit(20000), audit(log2), fraud(7, 5000)}, bankCharge(bank));
for (const Request& req : requests) late(req);
std::cout << "audit placed after the limit saw " << log2.size() / 2 << " of " << requests.size() << " requests\n";
}// middleware.js: each link gets the request and "next", and decides whether to call it
class Request {
constructor(user, amount, ageDays) {
this.user = user;
this.amount = amount; // rupees
this.ageDays = ageDays; // days since the account opened
}
}
const verdict = (status, reason) => ({ status, reason }); // approved, declined or held
class Bank {
// A stand-in for the bank: it only knows balances.
constructor(balances) {
this.balances = balances;
}
balance(user) {
return this.balances[user];
}
}
const audit = (log) => (req, next) => {
// runs before and after the rest of the chain
log.push(`in ${req.user} ${req.amount}`);
const v = next(req);
log.push(`out ${req.user} ${v.status}`);
return v;
};
const limit = (maxAmount) => (req, next) => {
if (req.amount > maxAmount) {
return verdict("declined", `over the limit of ${maxAmount}`); // stops here: next is never called
}
return next(req);
};
const fraud = (newDays, big) => (req, next) => {
if (req.ageDays < newDays && req.amount > big) {
return verdict("held", `new account, over ${big}`);
}
return next(req);
};
const bankCharge = (bank) => (req) => {
// the last link: no next
if (bank.balance(req.user) >= req.amount) {
return verdict("approved", "charged");
}
return verdict("declined", "insufficient funds");
};
function build(links, end) {
// fold the links from the end, so the first link runs first
return links.reduceRight((next, link) => (req) => link(req, next), end);
}
const bank = new Bank({ asha: 5000, ravi: 90000, meena: 9000 });
const REQUESTS = [new Request("asha", 450, 400), new Request("meena", 6000, 2), new Request("ravi", 25000, 400)];
const log = [];
const pay = build([audit(log), limit(20000), fraud(7, 5000)], bankCharge(bank));
for (const req of REQUESTS) {
const v = pay(req);
console.log(`${req.user} ${req.amount}: ${v.status} (${v.reason})`);
}
console.log("audit log:");
for (const line of log) {
console.log(line);
}
const log2 = [];
const late = build([limit(20000), audit(log2), fraud(7, 5000)], bankCharge(bank));
for (const req of REQUESTS) {
late(req);
}
console.log(`audit placed after the limit saw ${log2.length / 2} of ${REQUESTS.length} requests`);asha 450: approved (charged)
meena 6000: held (new account, over 5000)
ravi 25000: declined (over the limit of 20000)
audit log:
in asha 450
out asha approved
in meena 6000
out meena held
in ravi 25000
out ravi declined
audit placed after the limit saw 2 of 3 requestsRead the log. The audit link sits first, so it sees every request go in and every verdict come out, including Ravi's, which the very next link, the limit check, stopped. A link calls next to continue and returns without calling it to stop, as limit does. The last line shows why position matters: put audit after the limit check and it sees only the 2 requests that got past it, not the declined one.
Linked handlers: the classic form
- A handler returns a verdict or nothing; the base class passes the request on
- A handler can't act after the rest of the chain has run
- The chain can be assembled in pieces:
limit.then(fraud).then(bank) - The base class guarantees the request is passed on or stopped, never lost
Middleware: a function around the rest
- A link calls
nextitself, or doesn't: more control, and more ways to get it wrong - A link can run code before and after: timing, audit, retries, transactions
- The chain is built once, from a list, folded from the end
- A link that forgets to call
nextsilently ends the chain
The logger's chain sits between the two: a base handle calls process, which returns the record to pass on, possibly changed, or None to stop. So its links can transform the request (the redactor returns a cleaned copy), which the classic textbook chain doesn't do, but they can't act after the rest has run, as middleware can.
In middleware an audit link logs a request going in, calls next, and logs the verdict coming out, even when a later link stopped the request.
Chain of Responsibility and its look-alikes#
| Pattern | Looks like a chain because | The difference |
|---|---|---|
| Decorator (dp-11) | an object wraps another and forwards to it | A decorator always forwards and adds behaviour. A handler's whole job is to decide whether to stop. Middleware blurs the line: the audit link, which always calls next, is a decorator in all but name, and limit, which may not, is a handler. |
| Observer (dp-17) | one request reaches several objects | Every listener gets the event and none can stop it for the others. In a chain, one handler can end it. |
| Strategy (dp-16) | one request, several objects that could answer | A strategy is chosen from outside and exactly one runs. A chain visits several, in order. |
| Pipeline | a request moves through stages | In a pipeline every stage runs and passes its output on. In a chain any link may stop it. Middleware is a chain whose links usually pass on. |
| Composite (dp-13) | a request moves along a parent link | A chain can follow a tree upward (a widget asks its parent), but the point there is the tree, not the stopping. |
When not to use it#
- There are two checks and no sign of a third. An
ifwith an early return is clearer than three classes. - Every check must run and report. If you need all the failures at once (form validation), a list of validators that collects errors does the job; a chain stops at the first.
- The order must never vary and nobody else edits it. A function in a fixed order is fine; extract the chain when the order or the set of checks starts varying.
- A request must be handled by exactly one known receiver. That is a lookup, as in a registry or a dispatch table, not a chain.
- The chain is deep and slow. A long linked chain makes each request walk it, and an exception deep in it has a long stack. Keep chains short, or use a list.
Where it shows up in the case studies#
Only one round names the pattern, and it uses a variant:
| Case study | The chain | The variant |
|---|---|---|
| Pub-sub logger | Processor → LevelFilter → Redactor → Publish, built once by LogManager and extended with then() |
A pipeline chain: each link returns the record to pass on (possibly changed) or None to stop. A sampler ("keep 1 in 100 debug lines") is a new link. The lesson's callout also shows the textbook levels as a chain (ErrorHandler → WarnHandler → InfoHandler) and explains why it prefers a chain for processing and pub-sub for delivery. |
No other lesson uses it. The shape would fit anywhere a list of independent checks guards one action; the lesson to remember is the logger's, where adding a link needed no change to the others.
In the interview#
Say the pattern with the reason, in one breath: "The payment goes through limits, then fraud, then the bank, and any of them can stop it, so I'll make each check a handler and chain them: each either answers or passes the request on, the order is decided where the chain is built, and a missing answer fails closed." The last clause is what makes it a design, not a diagram.
Then expect the follow-ups:
- "Add a blocked-country check." A new handler class, inserted with one
then()call. No existing handler changes. - "Trusted customers skip fraud." Build a second chain without the fraud handler, rather than a flag in the handler.
- "Does the order matter?" Yes, and say how: the cheap, decisive checks go first, and when two checks disagree the order picks the answer. Test that case.
- "What if a handler throws?" Decide it deliberately. For a payment, fail closed: turn it into a declined or held verdict at the edge of the chain, and don't swallow it silently inside a handler.
- "What if a check is slow or async?" Put it late, give it a timeout, and let the verdict say timed out. Cheap checks first keep most requests from ever paying for it.
- "How do you test it?" Test each handler's
checkalone with a hand-made request, then test the chain's order with fake handlers that record the order they ran in, and the unhandled case.
Common mistakes#
- No end of the chain. A request that falls off the end with no answer should be a decision (decline, or a default verdict), never a silent approval.
- A handler that forgets to pass on. In middleware, a link that doesn't call
nextends the chain by accident; in the classic form, put the passing on in the base class. - Order by accident. The order changes the answers and the cost. Decide it, comment it, test it.
- A handler that calls the next one itself and the base class too. The request is then handled twice.
- Handlers that depend on each other. If
FraudCheckonly works afterLimitCheckhas changed the request, the chain is hiding a coupling; pass what it needs in the request. - A cycle. If
a.then(b)andb.then(a), the request never ends. Build chains in one place, from a list. - Using it for "all must run". A chain stops at the first answer; use a list of validators to collect every error.
A request that falls off the end of a chain has not been decided: treating that silence as approval is the classic mistake, so fail closed.
Chain of Responsibility
Q1/4 In the lesson's chain, what makes a request stop at a handler?
Answer & explanation
Answer: B
A handler that returns a verdict has decided, so the base class returns it and does not pass the request on. Returning nothing means "not mine", and the base class passes it to the next handler.
Q2/4 The fraud check calls a slow service and the limit check is one comparison. Which order is better, and why?
Answer & explanation
Answer: B
A chain stops at the first answer, so a cheap, decisive check first saves the slow ones. The order is also behaviour: if a request fails both, the first handler decides which answer the customer sees.
Q3/4 How do you tell a chain from a Decorator, when both forward to the next object?
Answer & explanation
Answer: C
A decorator wraps an object to add something and always calls it. A handler's job is the decision to stop or pass on, and some requests never reach the later handlers.
Q4/4 A chain has no bank at the end, and a normal payment passes every check. What should the chain answer?
Answer & explanation
Answer: C
A request that falls off the end has not been decided. For anything that moves money, fail closed: return an explicit unhandled or declined verdict, or end the chain with a handler that always answers.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


