Chain of Responsibility Pattern
Definitionβ
The Chain of Responsibility avoids coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. The receiving objects are chained, and the request passes along the chain until an object handles it.
Coffee Shop Exampleβ
Before an order is accepted, it goes through a validation pipeline: stock check β size availability β daily limit β payment authorization. Each handler checks one concern and either passes the request to the next handler or rejects it.
Structureβ
Handler Interfaceβ
public record OrderRequest(string Customer, string Coffee, CupSize Size, decimal Price);
public interface IOrderHandler
{
IOrderHandler SetNext(IOrderHandler handler);
string Handle(OrderRequest order);
}
Base Handlerβ
public abstract class OrderHandlerBase : IOrderHandler
{
private IOrderHandler? _next;
public IOrderHandler SetNext(IOrderHandler handler)
{
_next = handler;
return handler; // Enables fluent chaining
}
public virtual string Handle(OrderRequest order)
{
return _next?.Handle(order) ?? "β
Order approved β all checks passed!";
}
}
Concrete Handlersβ
public class StockCheckHandler : OrderHandlerBase
{
private readonly HashSet<string> _available = new() { "Latte", "Espresso", "Cappuccino" };
public override string Handle(OrderRequest order)
{
if (!_available.Contains(order.Coffee))
return $"β Rejected: {order.Coffee} is out of stock.";
Console.WriteLine($" [Stock] β {order.Coffee} is available");
return base.Handle(order);
}
}
public class SizeCheckHandler : OrderHandlerBase
{
private readonly Dictionary<string, CupSize> _maxSizes = new()
{
["Espresso"] = CupSize.Medium, // No large espresso
};
public override string Handle(OrderRequest order)
{
if (_maxSizes.TryGetValue(order.Coffee, out var maxSize) && order.Size > maxSize)
return $"β Rejected: {order.Coffee} only available up to {maxSize}.";
Console.WriteLine($" [Size] β {order.Size} {order.Coffee} is valid");
return base.Handle(order);
}
}
public class DailyLimitHandler : OrderHandlerBase
{
private readonly Dictionary<string, int> _ordersToday = new();
private readonly int _maxPerCustomer = 5;
public override string Handle(OrderRequest order)
{
var count = _ordersToday.GetValueOrDefault(order.Customer);
if (count >= _maxPerCustomer)
return $"β Rejected: {order.Customer} has reached the daily limit ({_maxPerCustomer}).";
_ordersToday[order.Customer] = count + 1;
Console.WriteLine($" [Limit] β {order.Customer} has ordered {count + 1}/{_maxPerCustomer} today");
return base.Handle(order);
}
}
public class PaymentHandler : OrderHandlerBase
{
private readonly decimal _maxOrderValue = 50.00m;
public override string Handle(OrderRequest order)
{
if (order.Price > _maxOrderValue)
return $"β Rejected: Order ${order.Price:F2} exceeds limit ${_maxOrderValue:F2}.";
Console.WriteLine($" [Payment] β ${order.Price:F2} within limits");
return base.Handle(order);
}
}
Client β Building the Chainβ
// Build the chain
var stock = new StockCheckHandler();
var size = new SizeCheckHandler();
var limit = new DailyLimitHandler();
var payment = new PaymentHandler();
stock.SetNext(size).SetNext(limit).SetNext(payment);
// Process orders
Console.WriteLine("--- Order 1: Normal Latte ---");
var result1 = stock.Handle(new OrderRequest("Alice", "Latte", CupSize.Large, 5.00m));
Console.WriteLine($" {result1}\n");
Console.WriteLine("--- Order 2: Large Espresso (not available) ---");
var result2 = stock.Handle(new OrderRequest("Bob", "Espresso", CupSize.Large, 3.50m));
Console.WriteLine($" {result2}\n");
Console.WriteLine("--- Order 3: Unknown coffee ---");
var result3 = stock.Handle(new OrderRequest("Carol", "Mocha", CupSize.Medium, 4.00m));
Console.WriteLine($" {result3}\n");
// Output:
// --- Order 1: Normal Latte ---
// [Stock] β Latte is available
// [Size] β Large Latte is valid
// [Limit] β Alice has ordered 1/5 today
// [Payment] β $5.00 within limits
// β
Order approved β all checks passed!
//
// --- Order 2: Large Espresso (not available) ---
// [Stock] β Espresso is available
// β Rejected: Espresso only available up to Medium.
//
// --- Order 3: Unknown coffee ---
// β Rejected: Mocha is out of stock.
Each handler does one thing (SRP). The chain is built by the client β you can reorder, add, or remove handlers without changing any handler's code. The sender only knows about the first handler in the chain.
Chain of Responsibility vs Decoratorβ
| Chain of Responsibility | Decorator | |
|---|---|---|
| Intent | Only one handler processes the request | Every decorator processes the request |
| Flow | Short-circuits on first match | Always passes through all |
| Return | Usually returns a result | Usually enhances and forwards |
.NET Real-World Usageβ
- ASP.NET Core Middleware Pipeline β each middleware handles the request or passes to the next
DelegatingHandlerchain inHttpClientβ message handlers in a pipeline- Exception handling middleware β try handlers in order until one catches
- Logging pipeline β different log handlers (console, file, remote) in a chain
- Authorization policies β check policies in order
When to Useβ
- More than one object may handle a request, and the handler isn't known in advance
- You want to issue a request to several objects without specifying the receiver explicitly
- The set of handlers should be configurable at runtime
When NOT to Useβ
- There's only one handler β no chain needed
- The order of handlers doesn't matter β a simple list of validators suffices
- Every handler must process the request β use Decorator instead
Key Takeawaysβ
- Chain of Responsibility decouples sender from receiver
- Each handler checks one condition and passes or rejects
- The chain is configurable β add, remove, or reorder handlers
- The pattern short-circuits: once a handler rejects, the chain stops
Interview Questionsβ
Q: How is ASP.NET Core middleware related to Chain of Responsibility?
ASP.NET Core middleware is Chain of Responsibility. Each middleware receives HttpContext, does its work, and calls next() to pass to the next middleware. The next delegate is the chain link. Middleware can short-circuit by not calling next().
Q: Can multiple handlers in the chain all process the request? Yes β the classic pattern says "one handler processes it," but a variation lets every handler process the request (like ASP.NET middleware). The key is that each handler decides whether to pass the request along.
Q: How do you ensure the chain always has a terminator?
Add a catch-all handler at the end that either approves the request or returns a default response. Without it, unhandled requests silently succeed (returning null or the base handler's default).
Related Topicsβ
- Decorator β every decorator processes, chain short-circuits
- Mediator β centralized routing vs decentralized chain
- Middleware β ASP.NET Core's implementation of this pattern
- SOLID β SRP & OCP β each handler has one responsibility, new handlers don't break the chain