The Chain of Responsibility pattern passes requests along a chain of handlers. Each handler either processes the request or hands it to the next one. Reach for it when you don't know in advance which object should handle a request, or when more than one object should get a chance to handle it.
The problem
You need to issue a request but don't know which object should handle it. If you hardcode the target, adding a handler means editing existing code, which breaks the Open/Closed principle. A long if-else chain has the same problem.
Chain of Responsibility decouples the sender from the receiver. The request travels through a chain of objects until one of them handles it.
The structure
The players
| Role | Responsibility |
|---|---|
| Handler | Interface declaring a method for handling requests and optionally passing to the next handler |
| ConcreteHandler | Implements the handling logic; decides whether to process the request or pass it on |
| Client | Initiates the request on the chain (doesn't know or care who handles it) |
A simple chain
A request flows from one handler to the next until someone processes it:
The decision at each handler
The same flow, with the check each handler makes:
Classic implementation
A typical chain either links handlers explicitly or gives each concrete handler a reference to the next one.
The code (C++)
#include <iostream>
#include <memory>
#include <string>
// ── Request class ────────────────────────────────────────
class Request {
private:
std::string type;
int priority;
public:
Request(const std::string& t, int p) : type(t), priority(p) {}
std::string getType() const { return type; }
int getPriority() const { return priority; }
};
// ── Handler interface ────────────────────────────────────
class Handler {
protected:
std::shared_ptr<Handler> nextHandler;
public:
virtual ~Handler() = default;
void setNext(std::shared_ptr<Handler> handler) {
nextHandler = handler;
}
virtual void handle(const Request& request) = 0;
};
// ── Concrete handlers ────────────────────────────────────
class SupportHandler : public Handler {
public:
void handle(const Request& request) override {
if (request.getType() == "technical") {
std::cout << "Support team handling technical issue\n";
} else if (nextHandler) {
nextHandler->handle(request);
} else {
std::cout << "No handler found for: " << request.getType() << "\n";
}
}
};
class ManagerHandler : public Handler {
public:
void handle(const Request& request) override {
if (request.getType() == "complaint") {
std::cout << "Manager handling complaint\n";
} else if (nextHandler) {
nextHandler->handle(request);
} else {
std::cout << "No handler found for: " << request.getType() << "\n";
}
}
};
class DirectorHandler : public Handler {
public:
void handle(const Request& request) override {
if (request.getPriority() > 8) {
std::cout << "Director handling high-priority request\n";
} else if (nextHandler) {
nextHandler->handle(request);
} else {
std::cout << "No handler found for: " << request.getType() << "\n";
}
}
};
// ── Client ──────────────────────────────────────────────
int main() {
auto support = std::make_shared<SupportHandler>();
auto manager = std::make_shared<ManagerHandler>();
auto director = std::make_shared<DirectorHandler>();
// Build the chain: support -> manager -> director
support->setNext(manager);
manager->setNext(director);
// Test requests
Request tech("technical", 5);
Request complaint("complaint", 3);
Request critical("escalation", 9);
std::cout << "=== Handling Requests ===\n";
support->handle(tech); // Handled by SupportHandler
support->handle(complaint); // Passed to ManagerHandler
support->handle(critical); // Passed to DirectorHandler
return 0;
}
How it works
- Client sends a request to the first handler in the chain.
- Each handler checks whether it can handle the request:
- If yes, process it and optionally stop the chain.
- If no, pass it to the next handler.
- Last handler either handles the request or reports that nobody could.
Building the chain
Two approaches build and maintain a chain.
Explicit links
Handlers keep an explicit reference to the next handler through a setNext() method or a constructor. That is the classic approach shown above. You get full control over the chain and can rearrange it at runtime:
// Manual chain construction
auto support = std::make_shared<SupportHandler>();
auto manager = std::make_shared<ManagerHandler>();
auto director = std::make_shared<DirectorHandler>();
support->setNext(manager);
manager->setNext(director);
// Chain: support → manager → director
Advantages: - Explicit, clear chain structure - Easy to modify or rearrange handlers at runtime - Supports multiple independent chains
Disadvantages:
- Needs an extra pointer (nextHandler)
- Handlers must know about linking and implement it
Reusing existing relationships
You can also reuse an existing object hierarchy instead of building links. In Qt, the parent-child widget hierarchy already does the job:
// Using Qt's parent-child hierarchy
QWidget* window = new QWidget();
QWidget* panel = new QWidget(window); // panel is child of window
QWidget* button = new QPushButton(panel); // button is child of panel
// Events bubble up: button → panel → window
// No setNext() call: the hierarchy is the chain
bool eventHandler(QEvent* event) {
// Check if we handle it
if (shouldHandle(event)) {
return true; // Handled
}
// Otherwise, Qt automatically passes to parent
return QWidget::event(event);
}
Advantages: - No extra code to maintain the chain - The chain structure already exists (widget hierarchy, tree structure) - Natural for frameworks built on hierarchies
Disadvantages: - Less flexible. The chain is locked to the hierarchy. - Not every hierarchy is suitable for chain semantics
Who drives the chain
Something has to drive the chain: either the handlers themselves or an outside object.
Internal chain
Each handler decides whether to pass the request on, so the chain drives itself:
class Handler {
protected:
std::shared_ptr<Handler> nextHandler;
public:
virtual void handle(const Request& request) = 0;
};
class SupportHandler : public Handler {
public:
void handle(const Request& request) override {
if (canHandle(request)) {
processRequest(request);
// Chain stops here
} else if (nextHandler) {
nextHandler->handle(request); // Chain continues
}
}
};
The request moves from handler to handler according to logic inside each one. This is the form in the classic example.
External chain
An orchestrator outside the handlers drives the request and decides the flow:
class Chain {
private:
std::vector<std::shared_ptr<Handler>> handlers;
public:
void addHandler(std::shared_ptr<Handler> h) {
handlers.push_back(h);
}
void process(const Request& request) {
for (auto& handler : handlers) {
if (handler->handle(request)) {
break; // Stop after first handler succeeds
}
}
}
};
// External orchestrator controls the flow
Chain chain;
chain.addHandler(support);
chain.addHandler(manager);
chain.addHandler(director);
chain.process(request); // Orchestrator drives the chain
Internal vs. external:
| Aspect | Internal | External |
|---|---|---|
| Control flow | Each handler decides the next step | Orchestrator controls the flow |
| Flexibility | Harder to change behavior | Easy to modify chain logic |
| Coupling | Handlers tightly coupled | Handlers independent |
| Visibility | Hidden flow, harder to debug | Explicit flow, easier to follow |
Multiple chains
Different handlers or orders give you different behavior. Useful cases:
- Priority-based handling: one chain for high-priority requests, another for normal priority
- Feature flags: different chains for different user groups
- Processing pipelines: different chains for different request types
Priority-based chains
// High-priority chain: director first, then manager
Chain highPriority;
highPriority.addHandler(director);
highPriority.addHandler(manager);
highPriority.addHandler(support);
// Normal-priority chain: support first, then manager
Chain normalPriority;
normalPriority.addHandler(support);
normalPriority.addHandler(manager);
normalPriority.addHandler(director);
// Route based on priority
if (request.getPriority() > 7) {
highPriority.process(request);
} else {
normalPriority.process(request);
}
This lets you: - Vary handler order per scenario - Build specialized chains for different use cases - Keep chains independent and testable
Variations
Logger chain
Sometimes every handler processes the request and passes it along. Logging is the usual example:
class Logger {
protected:
std::shared_ptr<Logger> next;
int level; // INFO, WARNING, ERROR
public:
virtual ~Logger() = default;
void setNext(std::shared_ptr<Logger> n) { next = n; }
void log(int logLevel, const std::string& message) {
if (logLevel >= level) {
writeLog(message);
}
if (next) {
next->log(logLevel, message);
}
}
virtual void writeLog(const std::string& message) = 0;
};
class ConsoleLogger : public Logger {
public:
ConsoleLogger() { level = 1; }
void writeLog(const std::string& message) override {
std::cout << "[CONSOLE] " << message << "\n";
}
};
class FileLogger : public Logger {
public:
FileLogger() { level = 2; }
void writeLog(const std::string& message) override {
std::cout << "[FILE] " << message << "\n";
}
};
When to use Chain of Responsibility
- Multiple handlers: several objects may handle a request, and you don't know which one in advance.
- Dynamic chain: the set of handlers or their order may change at runtime.
- Separation of concerns: decoupling sender from receiver reduces dependencies.
- Processing pipeline: handlers process a request in sequence (logging, validation, execution).
Real-world examples
- Event handling in GUI frameworks (mouse click bubbles up the component hierarchy)
- HTTP middleware in web frameworks (request passes through layers like auth, logging, compression)
- Request approval workflows (employee request → manager → director → executive)
- Exception handling in some languages where an exception can be caught at multiple levels
- Logger frameworks with multiple appenders (console, file, database)
Advantages and disadvantages
| Aspect | Pro | Con |
|---|---|---|
| Decoupling | Sender doesn't need to know handlers | Order of handlers matters; hard to debug |
| Flexibility | Easy to add or remove handlers at runtime | Request may not be handled at all |
| Single Responsibility | Each handler does one thing | More classes needed for simple requests |
| Open/Closed | Add new handlers without modifying existing ones | Handlers tightly coupled through nextHandler |
Summary
- Chain of Responsibility passes a request through a sequence of handlers until one processes it.
- Each handler either handles the request and stops, or passes it on.
- It decouples sender and receiver and supports dynamic workflows, but implicit routing makes debugging harder.
- You will meet it in middleware, event systems, and approval workflows.
Further reading
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF)
- Command Pattern, often paired with Chain of Responsibility
- Observer Pattern, an alternative for notifying multiple objects