Chain of Responsibility Pattern: Pass the Request Until Someone Handles It
Learn the Chain of Responsibility pattern with a simple school leave application story, easy TypeScript and C# code, diagrams, tables, and practice tasks.
๐ Priya's leave application
Meet Priya. She studies in Class 7B at Sunrise Public School in Kolkata. Her cousin Anushka is getting married in Jaipur next week. The whole family is going. Priya needs three days of leave from school.
On Monday morning, Priya writes a neat leave application. She folds it carefully and gives it to her class teacher, Mrs. Sen, before the first bell.
Now watch what happens to that little piece of paper.
Mrs. Sen reads it during the lunch break. School rules say a class teacher can approve leave of one day only. Three days is above her limit. Does she reject it? No. Does she call Priya and scold her? No. She simply writes "Forwarded" in the corner and sends it to the vice principal, Mr. Iyer.
Mr. Iyer can approve up to two days. Three days is still too much for him. So he also writes "Forwarded" and sends the letter up to the principal, Mrs. Rao.
Mrs. Rao can approve any reasonable leave. She reads the application, remembers her own cousin's wedding years ago, smiles, and signs it. Approved! The letter travels back down, and on Tuesday Priya finds out she is going to Jaipur.
Notice some important things in this small story:
- Priya gave the application to only one person โ Mrs. Sen.
- Priya did not know, and did not care, who would finally approve it.
- Each person in the line had a simple choice: handle it myself, or pass it ahead.
- The application travelled along a chain: Mrs. Sen โ Mr. Iyer โ Mrs. Rao.
- Nobody in the chain needed to know the full school rule book. Each one knew only their own limit.
This is exactly the Chain of Responsibility design pattern. You hand a request to the first link of a chain. The request travels link by link until someone takes responsibility for it. Keep Priya's letter in your mind โ every section of this lesson follows that same letter.
Here is the letter's path as a flow:
And here is the same trip told as a journey, with how each person feels along the way:
๐ฏ What is the Chain of Responsibility pattern?
Chain of Responsibility (also called CoR or Chain of Command) is a behavioral design pattern. Behavioral patterns are about how objects talk to each other and share work.
Here is a simple definition you can remember for exams:
Chain of Responsibility lets you pass a request along a line of handler objects. Each handler decides either to process the request or to pass it to the next handler in the line. The sender does not know which handler will finally process it.
The pattern was first described in the famous Gang of Four (GoF) book, Design Patterns: Elements of Reusable Object-Oriented Software (1994). It is one of the eleven behavioral patterns in that book.
Three small ideas make this pattern work:
- One common interface. Every handler looks the same from outside. It has one main method, usually called
handle(request). Mrs. Sen, Mr. Iyer, and Mrs. Rao all "look the same" to the letter โ each one reads it and decides. - A pointer to the next handler. Each handler keeps a reference to the next link. Mrs. Sen knows the letter goes to Mr. Iyer next. That pointer is what makes them a chain instead of three separate people.
- A choice at every link. Handle and stop, or pass it ahead. Sometimes a handler does a bit of work and still passes it ahead โ like Mrs. Sen noting "attendance is good" on the letter before forwarding it.
Match the pattern's roles with the school story:
| Pattern role | What it does | School version |
|---|---|---|
| Handler interface | Declares setNext() and handle() | The idea of "a person who can receive a leave letter" |
| Base handler | Stores the next link, forwards by default | The school habit of writing "Forwarded" |
| Concrete handlers | The real checking logic, one per rule | Mrs. Sen, Mr. Iyer, Mrs. Rao |
| Client | Wires the chain and sends the request | Priya (and the school office that decided the order) |
| Request | The thing travelling through the chain | The leave application itself |
Easy memory trick: think of the school line โ "Teacher can't? Pass it up!" The sender talks to only the first link. The chain decides the rest. If you can retell Priya's story, you already understand this pattern.
Here is the whole pattern as one mind map you can redraw in your notebook before an exam:
๐ The problem it solves
Why do we even need this pattern? Let us see the pain first.
Suppose the school hires you to write its leave approval software, and you write it the quick way โ one big function:
// โ The "one giant function" approach โ works today, hurts tomorrow
function approveLeave(days: number, studentName: string): string {
if (days <= 1) {
return `Class Teacher approved ${days} day leave for ${studentName}`;
} else if (days <= 2) {
return `Vice Principal approved ${days} day leave for ${studentName}`;
} else if (days <= 5) {
return `Principal approved ${days} day leave for ${studentName}`;
} else {
return `Leave rejected. Too many days!`;
}
}This looks fine for a small case. But real software grows the way real schools grow. Within one term, the school adds new rules:
- Sports leave must go to the Sports Teacher first.
- Medical leave above five days needs the School Board.
- During exam week, all leave must pass an extra "exam check".
- Day scholars and hostel students follow different first steps.
Now your one function becomes a jungle of if, else if, and nested conditions. Every new rule means editing this same function. Editing it risks breaking the old rules. Testing one rule forces you to think about every other rule. Worst of all, the order of checks is frozen inside the function. You cannot reorder or reuse a check without surgery.
The deepest problem has a name: tight coupling between the sender and the receivers. The code that sends the request is glued directly to the full list of approvers and their exact order. Priya, in real life, never needed to know that list โ she just handed the letter to Mrs. Sen. Your code should enjoy the same comfort.
Chain of Responsibility fixes this by giving each rule its own small handler class. Each handler does one job. You connect them in any order you like, at runtime, like joining train coaches. New rule? New coach. New order? Re-couple the coaches. The engine (the client) never changes.
โ๏ธ How it works, step by step
Let us build the pattern slowly, piece by piece, following the letter.
Step 1: Make a common Handler interface. It declares setNext(handler) to join the chain and handle(request) to process a request.
Step 2: Make a base handler class (optional but very helpful). It stores the next handler and gives default behavior: "if I have a next, forward to it; otherwise stop." Concrete handlers then only write their own special logic. This base class is the school habit of writing "Forwarded" โ nobody has to re-learn it.
Step 3: Write one concrete handler per rule. ClassTeacher, VicePrincipal, Principal. Each handler checks the request. If it can handle it, it does so and the chain stops. If not, it calls the next handler.
Step 4: Wire the chain in the client. classTeacher.setNext(vicePrincipal).setNext(principal). This is just one line. Want a new order? Change the wiring, not the handlers.
Step 5: Fire the request at the head. The client calls classTeacher.handle(request) and waits for the result.
Here is the journey of Priya's three-day application through the chain, message by message:
See how Priya only talks to Mrs. Sen. The rest happens inside the chain. If tomorrow the school adds a Head of Department between the vice principal and the principal, Priya's part of the story does not change at all. Only the wiring changes.
It also helps to see the states the application passes through. At any moment, the letter is sitting on exactly one desk:
One more important point: a request can end in three ways.
- Handled mid-chain. Someone approves or rejects it. The chain stops.
- Falls off the end. Nobody handled it. The client must plan for this (return
null, a default answer, or an error). - Processed by many links. In some versions, every handler does a small piece of work and passes it on anyway. Middleware works like this โ more on that in the College corner below.
College corner: this third style is exactly how a middleware pipeline works in web frameworks. In ASP.NET Core or Express.js, an HTTP request enters a chain where every link usually runs: logging middleware notes the request and passes it on, authentication middleware attaches the user and passes it on, and the final handler produces the response. The chain only short-circuits when something is wrong โ an auth middleware can return 401 Unauthorized without calling the next link, killing the request right there. So middleware is Chain of Responsibility tuned for "everyone does a bit" with an emergency brake at every link. When you configure middleware order in Program.cs or with app.use(...), you are literally wiring a chain.
๐ป Real-life code example
Now let us write Priya's school in TypeScript. Read the comments โ they tell the story.
// The request object โ the leave application itself
interface LeaveRequest {
studentName: string;
days: number;
reason: string;
}
// Step 1: Common interface for every approver in the chain
interface LeaveHandler {
setNext(handler: LeaveHandler): LeaveHandler;
handle(request: LeaveRequest): string | null;
}
// Step 2: Base class โ stores "next" and forwards by default
abstract class BaseApprover implements LeaveHandler {
private next: LeaveHandler | null = null;
setNext(handler: LeaveHandler): LeaveHandler {
this.next = handler;
return handler; // returning next lets us chain calls fluently
}
handle(request: LeaveRequest): string | null {
if (this.next) {
return this.next.handle(request); // forward to the next link
}
return null; // end of chain โ nobody handled it
}
}
// Step 3: Concrete handlers โ one small class per approver
class ClassTeacher extends BaseApprover {
handle(request: LeaveRequest): string | null {
if (request.days <= 1) {
return `Mrs. Sen approved ${request.days}-day leave for ${request.studentName}.`;
}
console.log("Mrs. Sen: above my limit, passing up...");
return super.handle(request); // pass to the next approver
}
}
class VicePrincipal extends BaseApprover {
handle(request: LeaveRequest): string | null {
if (request.days <= 2) {
return `Mr. Iyer approved ${request.days}-day leave for ${request.studentName}.`;
}
console.log("Mr. Iyer: above my limit, passing up...");
return super.handle(request);
}
}
class Principal extends BaseApprover {
handle(request: LeaveRequest): string | null {
if (request.days <= 5) {
return `Mrs. Rao approved ${request.days}-day leave for ${request.studentName}.`;
}
console.log("Mrs. Rao: even I cannot approve this much!");
return super.handle(request);
}
}
// Step 4: Client wires the chain โ order is just the wiring order
const classTeacher = new ClassTeacher();
const vicePrincipal = new VicePrincipal();
const principal = new Principal();
classTeacher.setNext(vicePrincipal).setNext(principal);
// Step 5: Fire requests at the head of the chain
const requests: LeaveRequest[] = [
{ studentName: "Riya", days: 1, reason: "Doctor visit" },
{ studentName: "Priya", days: 3, reason: "Cousin Anushka's wedding" },
{ studentName: "Meera", days: 10, reason: "Family trip" },
];
for (const req of requests) {
console.log(`\n--- ${req.studentName} asks for ${req.days} day(s) ---`);
const result = classTeacher.handle(req);
console.log(result ?? "Nobody could approve. Please meet the school board.");
}Output:
--- Riya asks for 1 day(s) ---
Mrs. Sen approved 1-day leave for Riya.
--- Priya asks for 3 day(s) ---
Mrs. Sen: above my limit, passing up...
Mr. Iyer: above my limit, passing up...
Mrs. Rao approved 3-day leave for Priya.
--- Meera asks for 10 day(s) ---
Mrs. Sen: above my limit, passing up...
Mr. Iyer: above my limit, passing up...
Mrs. Rao: even I cannot approve this much!
Nobody could approve. Please meet the school board.Look at Meera's case. Her request fell off the end of the chain, and our client handled that politely with a default message. Always plan for the "nobody handled it" case.
Also notice how easy change becomes. Want a Sports Teacher to check sports leave first? Write one new class and change one wiring line:
sportsTeacher.setNext(classTeacher).setNext(vicePrincipal).setNext(principal);No old handler was touched. This is the Open/Closed Principle in action: open for extension, closed for modification.
Here is the class structure you just built:
๐ฆ The same idea in C#
The pattern looks almost the same in C#. Here is a shorter version with the same school story:
public record LeaveRequest(string StudentName, int Days);
public abstract class Approver
{
private Approver? _next;
public Approver SetNext(Approver next)
{
_next = next;
return next;
}
public virtual string? Handle(LeaveRequest request)
=> _next?.Handle(request); // default: forward, or null at chain end
}
public class ClassTeacher : Approver
{
public override string? Handle(LeaveRequest request)
=> request.Days <= 1
? $"Class Teacher approved {request.Days}-day leave."
: base.Handle(request);
}
public class VicePrincipal : Approver
{
public override string? Handle(LeaveRequest request)
=> request.Days <= 2
? $"Vice Principal approved {request.Days}-day leave."
: base.Handle(request);
}
public class Principal : Approver
{
public override string? Handle(LeaveRequest request)
=> request.Days <= 5
? $"Principal approved {request.Days}-day leave."
: base.Handle(request);
}
// Client
var teacher = new ClassTeacher();
teacher.SetNext(new VicePrincipal()).SetNext(new Principal());
Console.WriteLine(teacher.Handle(new LeaveRequest("Priya", 3)));
// Output: Principal approved 3-day leave.And because Python students should not feel left out, here is the tiniest possible Python chain โ same story, duck typing instead of interfaces:
class Approver:
def __init__(self, limit: int, title: str):
self.limit = limit
self.title = title
self.next: "Approver | None" = None
def set_next(self, handler: "Approver") -> "Approver":
self.next = handler
return handler
def handle(self, days: int) -> str:
if days <= self.limit:
return f"{self.title} approved {days}-day leave."
if self.next:
return self.next.handle(days)
return "Nobody could approve. Meet the school board."
teacher = Approver(1, "Mrs. Sen")
teacher.set_next(Approver(2, "Mr. Iyer")).set_next(Approver(5, "Mrs. Rao"))
print(teacher.handle(3)) # Mrs. Rao approved 3-day leave.
print(teacher.handle(10)) # Nobody could approve. Meet the school board.College corner: C# developers meet this pattern every single day without noticing, because the ASP.NET Core middleware pipeline is built on the same idea. Each middleware receives a RequestDelegate named next. Calling await next(context) forwards the request; skipping that call short-circuits the pipeline. The order you register middleware (UseAuthentication before UseAuthorization, exception handling first) is exactly the wiring step from our pattern โ and getting that order wrong is one of the most common real-world ASP.NET bugs. Once you see middleware as a chain of handlers, the official docs suddenly read like this lesson.
๐ Where you see it in real software
Chain of Responsibility is not a textbook-only pattern. It quietly runs huge parts of the software world.
1. ASP.NET Core middleware. Every HTTP request entering an ASP.NET Core app passes through a pipeline of middleware components. Each middleware can do work before and after calling next, or it can short-circuit the pipeline by not calling next at all โ for example, an authentication middleware returning 401 immediately.
2. Express.js middleware. In Node.js, Express passes each request through functions of the shape (req, res, next). Calling next() forwards the request along the chain. Sending a response without calling next() stops the chain. Logging, body parsing, sessions, and routing are all links in this chain.
3. Java servlet filters. The Java Servlet specification defines Filter objects with a doFilter(request, response, chain) method. Each filter decides whether to call chain.doFilter(...) to continue. This design is decades old and still everywhere in Java web apps.
4. DOM event bubbling. When you click a button inside a div inside body, the click event "bubbles" upward: button โ div โ body โ document. Each element along the path gets a chance to handle the event, and any handler can call stopPropagation() to break the chain. The browser itself is running a Chain of Responsibility for you.
5. Logging frameworks. Loggers often pass a log record along handlers (console handler, file handler, email-on-error handler). Each handler checks the log level and decides what to do.
6. Support and escalation systems. Helpdesk software escalates tickets: bot โ level-1 agent โ level-2 engineer โ manager. Sound familiar? It is Priya's letter wearing a headset.
| Real software | What is the request? | Who are the handlers? | Can a link stop the chain? |
|---|---|---|---|
| ASP.NET Core pipeline | HTTP request | Middleware components | Yes โ skip calling next |
| Express.js | HTTP request | Middleware functions | Yes โ respond without next() |
| Java servlet filters | HTTP request | Filter objects | Yes โ skip chain.doFilter |
| DOM event bubbling | UI event (click, key) | Elements on the path | Yes โ stopPropagation() |
| Logging frameworks | Log record | Log handlers/appenders | Yes โ filter by level |
| Helpdesk escalation | Support ticket | Agents by level | Yes โ resolve at any level |
Interesting side note: in a healthy chain, most requests stop early. In Priya's school, most leave letters are one-day letters that Mrs. Sen approves herself. If the principal's desk received most letters, the limits would be badly designed. The same is true in software โ your first handlers should absorb the common cases.
And here is how far a letter travels depending on how big the ask is โ the bigger the request, the deeper it goes into the chain:
Notice the 10-day bar: it also visits all three handlers, but ends unhandled. Travelling the full chain does not guarantee an answer โ that is why the fallback message exists.
๐ค When to use it and when not to
Like every pattern, CoR is a tool, not a rule. Use this table to decide:
| Situation | Use it? | Why |
|---|---|---|
| Many objects may handle a request, and you do not know which one in advance | โ | The chain finds the right handler at runtime |
| You want to add, remove, or reorder processing steps without editing old code | โ | Wiring is separate from handler logic |
A long if / else if ladder decides "who processes what" | โ | Each branch becomes one clean handler |
| Several checks must run one after another (auth, validation, rate limit) | โ | Classic middleware-style chain |
| Exactly one fixed object always handles the request | โ | A direct method call is simpler and clearer |
| You only have two or three stable conditions that never change | โ | A small if/else is honest and readable |
| Every receiver must always get the request, no stopping allowed | โ | That is closer to Observer / events |
| You need a guaranteed answer for every request | โ ๏ธ | Possible, but you must add a default end-of-chain handler |
If you like pictures more than tables, place your own situation on this map. The further you are towards "many possible handlers" and "flow changes often", the stronger the case for a chain:
โ ๏ธ Common mistakes students make
Mistake 1: Forgetting the unhandled case. If no handler claims the request, it silently falls off the end. Beginners forget this and their program returns undefined or null with no message. Always decide what "nobody handled it" means: a default handler at the end, a clear error, or a polite fallback like our school-board message for Meera.
Mistake 2: Forgetting to call the next handler. Inside a concrete handler, if you forget return super.handle(request) (or next.handle(request)), the chain quietly stops at that link. Requests vanish and you spend hours wondering why. Make the base class do the forwarding, so concrete classes only override what they need.
A few more traps to watch for:
- Creating a circular chain. If handler A's next is B, and B's next is A, your request loops forever โ like a letter bouncing between two offices until the school year ends. Keep wiring in one place so you can see the whole chain at a glance.
- Putting too much logic in one handler. If a handler checks five different things, split it into five handlers. One job per link โ that is the whole point. Mrs. Sen checks days; she does not also verify medical certificates and exam schedules.
- Depending on chain order secretly. If
ValidationHandleronly works whenAuthHandlerran before it, write that down, or better, make handlers independent. - Building very long chains for tiny problems. Ten classes to replace three
ifstatements is over-engineering. Patterns should reduce pain, not add ceremony. - Mutating the request carelessly mid-chain. If a handler edits the request before passing it on, later handlers see changed data. Sometimes that is the design (middleware attaching a user). If it is not, keep requests read-only.
College corner: in production middleware pipelines, the debugging version of these mistakes is famous. A middleware that forgets to call next makes every request hang or return empty responses; a middleware registered after the router never runs at all; an exception-handling middleware registered last (instead of first) cannot catch anything upstream. When your framework "mysteriously ignores" a handler, draw the chain on paper first โ the wiring is almost always the bug, not the handler.
๐ช Compare with cousins
Chain of Responsibility has a few look-alike cousins in the behavioral family. Knowing the differences is a favourite exam and interview question.
| Pattern | How sender and receiver connect | Who receives the request? | Can the flow be stopped midway? |
|---|---|---|---|
| Chain of Responsibility | Sender โ first link โ next โ next | The first handler that claims it (usually one) | โ Yes, any link can stop it |
| Command | Sender wraps the request in an object | One known receiver, via the command | โ Not about stopping โ about storing/queuing/undoing |
| Observer | Publisher keeps a list of subscribers | All subscribers get every event | โ Everyone is notified |
| Mediator | Everyone talks to one central hub | The hub decides who acts | The hub controls routing |
| Decorator | Wrappers around one object, like a chain | Every wrapper always runs | โ Decorators must pass the call on |
The trickiest cousin is Decorator, because its structure (objects linked one after another) looks identical. Remember the difference by intent: a Decorator adds behavior and always forwards the call; a CoR handler decides and may refuse to forward. Decorators cannot break the chain. CoR handlers can. In school terms: a Decorator is like each office stamping the letter and always sending it ahead; CoR is like each office having the power to settle the matter and file the letter away.
Against Observer, think of the morning assembly announcement: everyone hears it (Observer). Priya's letter goes to exactly one final approver (CoR). Against Mediator, imagine if all letters went to the school office first, and the office decided who handles what โ that central desk is a Mediator, not a chain.
๐ฆ Quick revision box
+=====================================================================+
| CHAIN OF RESPONSIBILITY โ QUICK REVISION |
+=====================================================================+
| WHAT : Pass a request along a line of handlers; each one |
| handles it OR passes it to the next. |
| STORY : Priya's leave letter: Mrs. Sen -> Mr. Iyer -> Mrs. Rao. |
| Student talks only to the first link. |
| ROLES : Handler (interface) | BaseHandler (stores next) |
| ConcreteHandlers (real logic) | Client (wires chain) |
| KEY CODE : setNext(h) + handle(request) |
| ENDINGS : handled mid-chain | falls off the end | many links work |
| WINS : single responsibility, easy reorder, open/closed, |
| reusable handlers |
| RISKS : unhandled requests, harder debugging, long chains slow |
| SEEN IN : ASP.NET Core / Express middleware, servlet filters, |
| DOM event bubbling, logging frameworks |
| COUSIN : Decorator looks same but ALWAYS forwards; CoR may STOP. |
+=====================================================================+๐๏ธ Practice exercise
Try these three tasks yourself. Type the code โ do not just read it.
Task 1: Bank complaint escalation. A customer files a complaint with an amount involved. Build a chain: CustomerCareExecutive (handles complaints up to โน1,000) โ BranchManager (up to โน50,000) โ RegionalOfficer (up to โน10,00,000). If even the regional officer cannot handle it, print "Complaint forwarded to the banking ombudsman." Test with amounts โน500, โน20,000, and โน50,00,000.
Task 2: Exam-week rule. Add an ExamWeekHandler at the front of Priya's school leave chain. During exam week (isExamWeek = true on the request), it rejects every leave request except medical ones, without passing them ahead. Show that you did this by changing only the wiring line and adding one new class โ no old class should be edited. This proves you understand the Open/Closed Principle.
Task 3 (challenge): Mini middleware. Build a tiny request pipeline like Express: a LoggerHandler that prints the request and always passes it on, an AuthHandler that stops the chain with "401 Unauthorized" if request.token is missing, and a BusinessHandler that returns "200 OK". This shows the "every link does a bit of work" style of the pattern. Bonus: make LoggerHandler also print "request finished" after the rest of the chain returns โ that before/after sandwich is exactly how real middleware measures response time.
If you can finish Task 3, you have basically rebuilt the heart of every web framework's middleware system. Well done โ now pass this lesson on to the next student in your chain. If they cannot handle it, they know what to do: pass it up! ๐
Frequently asked questions
- What is the Chain of Responsibility pattern in simple words?
- It is a way to pass a request along a line of handler objects. Each handler checks the request and either handles it or passes it to the next handler. The sender never needs to know who will finally handle it.
- What happens if no handler in the chain handles the request?
- The request falls off the end of the chain unhandled. Good designs plan for this by returning null, a default result, or throwing a clear error so the client knows nobody took responsibility.
- How is Chain of Responsibility different from Decorator?
- Both link objects in a line, but a Decorator always passes the call onward and adds something extra, while a Chain of Responsibility handler can stop the request completely and not let it go further.
- Where is Chain of Responsibility used in real software?
- Web framework middleware is the most famous example. Express.js and ASP.NET Core both pass every HTTP request through a chain of middleware handlers. Java servlet filters and DOM event bubbling also work this way.
- Does every handler in the chain have to handle the request?
- No. Each handler decides for itself. It can handle the request and stop the chain, do some work and still pass it on, or simply pass it on untouched. That freedom is the heart of the pattern.
Further reading
Related Lessons
Command Pattern: Turn Every Action into an Order Slip
Learn the Command pattern through a restaurant order slip story. Simple TypeScript and C# code with undo and redo, diagrams, tables, and practice tasks.
Iterator Pattern: Visit Every Element, One Seat at a Time
Learn the Iterator pattern with a railway ticket checker story. Build a custom iterator in TypeScript, use for...of and generators, plus C# IEnumerable.
Decorator Pattern: Add Toppings to Your Objects, One Layer at a Time
Learn the Decorator pattern with a simple dosa-toppings story. Wrap objects in layers to add new behaviour at runtime, without making new subclasses.
Mediator Pattern: The Control Tower That Stops the Chaos
Learn the Mediator pattern with an airport control tower story, simple TypeScript and C# code, MediatR examples, diagrams, tables and easy practice tasks.