Chain of Responsibility is a pattern that passes a request through a chain of handlers until one processes it or until all have intervened, depending on the variant.
Imagine you work at a large company and need approval to buy a new laptop for €2,000.
- You ask your team lead, who can only approve expenses up to €500 and escalates to the manager.
- The request reaches the Manager. He says: “I only approve up to €1,500. This goes to the Director.”
- It reaches the Director. He says: “OK, €2,000 fits my budget. Approved.”
As a client, you don’t have to chase each responsible person. You submit the request to the first link and the chain decides how to proceed.
The Problem: The giant conditional
Without this pattern, the code that decides who processes a request often turns into a monster full of nested if/else statements.
// ❌ Tightly coupled and hard-to-maintain code
public void ProcessPurchase(Purchase purchase)
{
if (purchase.Amount < 500)
{
// Team Lead logic
}
else if (purchase.Amount < 1500)
{
// Manager logic
}
else if (purchase.Amount < 10000)
{
// Director logic
}
else
{
throw new Exception("No one can approve this.");
}
}The problem is obvious: the sender of the request knows all possible receivers and the exact decision logic. If we change the budget limits or add a new role (e.g., Vice President), we have to modify this central code.
The Solution: Pass it to the next
The pattern proposes turning each handler into an independent class. Each handler has two options:
- Process the request if it can.
- Pass the request to the next handler in the chain if it cannot.
Implementation in C#
Let’s implement the corporate expense approval system.
The Abstract Handler
This base class manages the “plumbing” of the chain (storing the next handler and calling it).
public abstract class Approver
{
protected Approver _next;
// Sets the next link and returns it
// (to allow fluent chaining)
public Approver SetNext(Approver next)
{
_next = next;
return next;
}
// The method that will decide
public abstract void Process(Purchase purchase);
}
// Simple data class
public class Purchase
{
public string Concept { get; set; }
public double Amount { get; set; }
}The Chain Links
Each class only worries about its own responsibility.
public class TeamLead : Approver
{
public override void Process(Purchase purchase)
{
if (purchase.Amount <= 500)
{
Console.WriteLine($"✅ Team Lead approves: {purchase.Concept} ({purchase.Amount}€)");
}
else if (_next != null)
{
Console.WriteLine("⏭️ Team Lead cannot. Passing to Manager...");
_next.Process(purchase);
}
}
}
public class Manager : Approver
{
public override void Process(Purchase purchase)
{
if (purchase.Amount <= 1500)
{
Console.WriteLine($"✅ Manager approves: {purchase.Concept} ({purchase.Amount}€)");
}
else if (_next != null)
{
Console.WriteLine("⏭️ Manager cannot. Passing to Director...");
_next.Process(purchase);
}
}
}
public class Director : Approver
{
public override void Process(Purchase purchase)
{
if (purchase.Amount <= 10000)
{
Console.WriteLine($"✅ Director approves: {purchase.Concept} ({purchase.Amount}€)");
}
else
{
Console.WriteLine($"❌ Purchase rejected: {purchase.Concept}. Amount {purchase.Amount}€ is too high.");
}
}
}The Client (Building the chain)
The client builds the chain once and uses it as many times as needed. Note that the order matters.
class Program
{
static void Main(string[] args)
{
// 1. Create the links
var teamLead = new TeamLead();
var manager = new Manager();
var director = new Director();
// 2. Chain them: TeamLead -> Manager -> Director
teamLead.SetNext(manager).SetNext(director);
// 3. Test various scenarios
Console.WriteLine("\n--- Attempt 1: Mouse (€50) ---");
teamLead.Process(new Purchase { Concept = "USB Mouse", Amount = 50 });
Console.WriteLine("\n--- Attempt 2: Laptop (€1200) ---");
teamLead.Process(new Purchase { Concept = "Dell Laptop", Amount = 1200 });
Console.WriteLine("\n--- Attempt 3: Server (€6000) ---");
teamLead.Process(new Purchase { Concept = "Rack Server", Amount = 6000 });
Console.WriteLine("\n--- Attempt 4: Yacht (€50000) ---");
teamLead.Process(new Purchase { Concept = "Company Yacht", Amount = 50000 });
}
}Output:
--- Attempt 1: Mouse (€50) ---
✅ Team Lead approves: USB Mouse (€50)
--- Attempt 2: Laptop (€1200) ---
⏭️ Team Lead cannot. Passing to Manager...
✅ Manager approves: Dell Laptop (€1200)
--- Attempt 3: Server (€6000) ---
⏭️ Team Lead cannot. Passing to Manager...
⏭️ Manager cannot. Passing to Director...
✅ Director approves: Rack Server (€6000)
--- Attempt 4: Yacht (€50000) ---
⏭️ Team Lead cannot. Passing to Manager...
⏭️ Manager cannot. Passing to Director...
❌ Purchase rejected: Company Yacht. Amount €50000 is too high.Variant: Middleware (ASP.NET Core)
There is a modern and very popular variant of this pattern where all links can process part of the request before passing it to the next. It does not stop at the first one that processes it.
This is exactly what the ASP.NET Core pipeline does.
// Conceptually in ASP.NET Core
app.Use(async (context, next) =>
{
Console.WriteLine("Middleware 1: Logging request");
await next(); // Calls the next middleware
Console.WriteLine("Middleware 1: Logging response");
});
app.Use(async (context, next) =>
{
Console.WriteLine("Middleware 2: Authenticating");
await next();
});Here the “chain” allows adding logging, authentication, compression, and error handling in a modular way.
When to use Chain of Responsibility?
- Multiple handlers: When more than one object can handle a request and you don’t know in advance which one will.
- Decoupling: When you want to send a request to an object without explicitly specifying the receiver.
- Dynamic Order: When the set of objects that can handle a request must be specified dynamically (you can change the chain order at runtime).
Chain of Responsibility is the pattern of efficient bureaucracy. It allows us to build complex decision flows by connecting simple pieces. If you ever see a method with 5 consecutive if statements checking who should do the work, turn it into a chain.