Chain of Responsibility Pattern
- Description: Pass a request along a chain of handlers — each decides to handle or forward, decoupling sender from receiver and avoiding monolithic if/else dispatch.
- My Notion Note ID: K2C-2-13
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Intent
- 2. Structure
- 3. C++ Example
- 4. When to Use / When Not To
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. Intent
- Decouple sender of a request from its receiver by chaining candidate handlers.
- Each handler — handle the request OR forward to the next.
- Replaces a giant
switch/if-else ifchain with composable links. - Caller doesn't know which handler will respond — or whether any will.
Typical uses:
- GUI event bubbling — child widget → parent → window → application.
- HTTP middleware — auth → logging → rate-limit → router.
- Logging filters — debug → info → warn → error sinks.
- Approval workflow — team lead → manager → director, by amount.
- Exception handling — language-level CoR (catch frames up the call stack).
2. Structure
Roles:
- Handler — interface declaring
handle(request)+ reference tonext. - ConcreteHandler — implements
handle; either consumes request or delegates tonext. - Client — builds chain, submits request to head.
Client → [HandlerA] → [HandlerB] → [HandlerC] → nullptr
│ handle? │ handle? │ handle?
└─ forward ──┴─ forward ──┴─ done / unhandled
Forwarding contract — classic GoF: return after handling OR pass to next. Modern variants: return bool (handled?), return std::optional<Result>, or always-forward (every handler runs, used for middleware pipelines).
3. C++ Example
3.1 Classic OO form
#include <memory>
#include <string>
#include <iostream>
struct Request {
std::string type;
int amount;
};
class Handler {
public:
virtual ~Handler() = default;
void set_next(std::shared_ptr<Handler> n) { next_ = std::move(n); }
virtual void handle(const Request& r) {
if (next_) next_->handle(r);
}
protected:
std::shared_ptr<Handler> next_;
};
class TeamLead : public Handler {
public:
void handle(const Request& r) override {
if (r.amount <= 1'000) std::cout << "TeamLead approves " << r.amount << "\n";
else Handler::handle(r);
}
};
class Manager : public Handler {
public:
void handle(const Request& r) override {
if (r.amount <= 10'000) std::cout << "Manager approves " << r.amount << "\n";
else Handler::handle(r);
}
};
class Director : public Handler {
public:
void handle(const Request& r) override {
if (r.amount <= 100'000) std::cout << "Director approves " << r.amount << "\n";
else std::cout << "Rejected: " << r.amount << " too large\n";
}
};
int main() {
auto lead = std::make_shared<TeamLead>();
auto mgr = std::make_shared<Manager>();
auto dir = std::make_shared<Director>();
lead->set_next(mgr);
mgr->set_next(dir);
lead->handle({"expense", 500});
lead->handle({"expense", 5'000});
lead->handle({"expense", 50'000});
lead->handle({"expense", 500'000});
}
3.2 Modern form — std::vector<std::function<bool(Req&)>>
For pipelines where each link is stateless, the OO scaffolding is overkill. Use a vector of callables; first one to return true consumes the request.
#include <functional>
#include <vector>
#include <string>
#include <iostream>
struct Req { std::string type; int amount; };
class Chain {
std::vector<std::function<bool(Req&)>> handlers_;
public:
Chain& add(std::function<bool(Req&)> h) {
handlers_.push_back(std::move(h));
return *this;
}
bool dispatch(Req r) {
for (auto& h : handlers_) if (h(r)) return true;
return false;
}
};
int main() {
Chain c;
c.add([](Req& r) { if (r.amount <= 1'000) { std::cout << "lead\n"; return true; } return false; })
.add([](Req& r) { if (r.amount <= 10'000) { std::cout << "mgr\n"; return true; } return false; })
.add([](Req& r) { if (r.amount <= 100'000) { std::cout << "dir\n"; return true; } return false; });
c.dispatch({"x", 5'000}); // mgr
c.dispatch({"x", 5'000'000}); // unhandled → false
}
3.3 Always-forward middleware pipeline
Each link processes + forwards (no early exit). Mirrors Express / ASP.NET middleware.
using Next = std::function<void()>;
using Middleware = std::function<void(Req&, Next)>;
class Pipeline {
std::vector<Middleware> mws_;
public:
Pipeline& use(Middleware m) { mws_.push_back(std::move(m)); return *this; }
void run(Req r) {
std::function<void(size_t)> step = [&](size_t i) {
if (i >= mws_.size()) return;
mws_[i](r, [&] { step(i + 1); });
};
step(0);
}
};
4. When to Use / When Not To
Use when:
- More than one object may handle a request, decided at runtime.
- Set of handlers should be configurable/orderable without recompile.
- You want to add/remove handlers dynamically.
- Sender shouldn't know which handler — only the entry point.
Don't use when:
- Exactly one handler always handles → direct call.
- Order of handlers doesn't matter and all must run → just iterate.
- Performance-critical hot path — virtual dispatch + pointer chasing per link.
- A simple
switch/ table lookup is clearer (rule of thumb: ≤3 cases, no future growth).
5. Variants and Pitfalls
Variants:
- Linked list chain (classic GoF) vs vector / pipeline (data-oriented).
- Tree CoR — Composite + CoR; request bubbles up parent pointers (GUI, DOM events).
- Stop on first hit vs always run all (middleware).
- Return value —
void(classic),bool(handled?),std::optional<T>, or fullResult<T, E>.
Pitfalls:
- Request falls off the end unhandled. Always have a terminal default handler or check the return value.
- Cycles.
A → B → Ainfinite loop. Validate chain on construction. - Lifetime. Raw
Handler*next-pointers + shared ownership of handlers → use cases dangling.shared_ptr/ arena allocation safer. - Hidden coupling via shared state. Handlers sharing a mutable context object reintroduce the coupling CoR was supposed to remove.
- Order matters but isn't obvious. Document chain assembly; consider builder pattern with named insertion points (
before("auth")).
6. Related Patterns
- Chain of Responsibility vs Decorator — both wrap + forward. CoR may not handle (early exit / no match → unhandled); Decorator always processes (adds behavior, then delegates). Structurally identical; intent differs.
- CoR vs Strategy — Strategy picks one algorithm up front; CoR tries handlers in order until one accepts. CoR = runtime selection by data; Strategy = config-time selection.
- CoR vs Command — Command encapsulates a request as an object; CoR routes it through handlers. Combined: enqueue a
Commandobject, dispatch through a CoR chain. - CoR vs Composite — Tree-CoR uses Composite to give each node a parent reference for bubbling.
- CoR vs Observer — Observer fans out one event to many independent listeners (all run, unordered). CoR is ordered, single-consumer.
7. References
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides (GoF), ch. 5.
- Refactoring.Guru — Chain of Responsibility
- cppreference —
std::function - ASP.NET Core middleware — concrete real-world CoR.
- Express.js middleware — JS equivalent.