Sponsored
Many thanks to the sponsors who make it possible for this newsletter to be free for readers. Become a sponsor.
The Chain of Responsibility pattern is a behavioral design pattern that allows you to build a chain of objects to handle a request or perform a task. Each object in the chain has the ability to either handle the request or pass it on to the next object in the chain. This pattern promotes loose coupling and flexibility in handling requests. Let's explore how to implement the Chain of Responsibility pattern in .NET using a practical example.
Let's consider a scenario where we have a series of discount rules for an e-commerce application. Depending on the customer's profile, we want to apply different discount percentages to their orders. The discount rules are as follows:
public decimal CalculateDiscount(Customer customer, decimal orderTotal){ if (customer.IsVIP) { return orderTotal * 0.8m; // 20% discount } else if (customer.IsRegular) { return orderTotal * 0.9m; // 10% discount } else if (customer.IsNew) { return orderTotal * 0.95m; // 5% discount } else { return orderTotal; // no discount }}
While the If statements approach works, it can become unwieldy when the number of rules grows. The Chain of Responsibility pattern provides a more flexible and maintainable solution. Let's refactor the code to use this pattern. Step #1: Create an abstract handler class, DiscountHandler, that defines a common interface for all discount handlers:
public abstract class DiscountHandler{ protected DiscountHandler _nextHandler; public DiscountHandler SetNextHandler(DiscountHandler nextHandler) { _nextHandler = nextHandler; return nextHandler; } public abstract decimal CalculateDiscount(Customer customer, decimal orderTotal);}
Step #2: Implement concrete discount handlers by deriving from DiscountHandler. Each handler will handle a specific rule and decide whether to apply a discount or pass the request to the next handler. VIPDiscountHandler:
public class VIPDiscountHandler : DiscountHandler{ public override decimal CalculateDiscount(Customer customer, decimal orderTotal) { if (customer.IsVIP) { return orderTotal * 0.8m; // 20% discount } return _nextHandler?.CalculateDiscount(customer, orderTotal) ?? orderTotal; }}
RegularDiscountHandler:
public class RegularDiscountHandler : DiscountHandler{ public override decimal CalculateDiscount(Customer customer, decimal orderTotal) { if (customer.IsRegular) { return orderTotal * 0.9m; // 10% discount } return _nextHandler?.CalculateDiscount(customer, orderTotal) ?? orderTotal; }}
NewCustomerDiscountHandler:
public class NewCustomerDiscountHandler : DiscountHandler{ public override decimal CalculateDiscount(Customer customer, decimal orderTotal) { if (customer.IsNew) { return orderTotal * 0.95m; // 5% discount } return _nextHandler?.CalculateDiscount(customer, orderTotal) ?? orderTotal; }}
NoDiscountHandler:
public class NoDiscountHandler : DiscountHandler{ public override decimal CalculateDiscount(Customer customer, decimal orderTotal) { return orderTotal; // no discount }}
Step #3: With the concrete handlers in place, we can create the chain by linking them together:
var vipHandler = new VIPDiscountHandler(); vipHandler.SetNextHandler(new RegularDiscountHandler()) .SetNextHandler(new NewCustomerDiscountHandler()) .SetNextHandler(new NoDiscountHandler());
Finally, we can invoke the chain by calling the CalculateDiscount method on the first handler in the chain:
decimal discountAmount = vipHandler.CalculateDiscount(customer, orderTotal);
What are the benefits from this?
1. Flexibility
The Chain of Responsibility pattern allows you to dynamically modify or extend the chain without affecting other parts of the code. You can add or remove handlers as needed.
2. Loose coupling
The pattern promotes loose coupling between the sender of a request and its receivers. Each handler only needs to know about its immediate successor, minimizing dependencies.
3. Single Responsibility Principle
You can decouple classes that invoke operations from classes that perform operations. Each handler does one thing and does it well.
4. Open/Closed Principle
You can introduce new handlers without changing existing ones. The chain grows without modifying the code that uses it.
1. Request may go unhandled
If none of the handlers in the chain can handle the request, it may go unhandled, leading to unexpected behavior. Always include a default handler at the end of the chain (like NoDiscountHandler above).
2. Potential performance impact
If the chain becomes very long, it may result in performance overhead due to the traversal of multiple handlers. In practice, this is rarely a problem unless you have dozens of handlers.
3. Debugging complexity
When something goes wrong, tracing which handler processed (or skipped) a request can be harder than reading a simple if-else block. Good logging inside each handler solves this.
This pattern shines when:
Real-world examples in .NET:
next(). This is the Chain of Responsibility pattern in action.The discount example is great for learning, but let me show you something closer to production code.
Imagine you have an API endpoint that creates orders. Before processing, you need to validate the request through multiple steps:
public abstract class OrderValidationHandler{ protected OrderValidationHandler _next; public OrderValidationHandler SetNext(OrderValidationHandler next) { _next = next; return next; } public abstract Result Validate(CreateOrderRequest request); protected Result CallNext(CreateOrderRequest request) { return _next?.Validate(request) ?? Result.Success(); }}
Each handler checks one thing:
public class StockAvailabilityHandler : OrderValidationHandler{ private readonly IInventoryService _inventory; public StockAvailabilityHandler(IInventoryService inventory) { _inventory = inventory; } public override Result Validate(CreateOrderRequest request) { foreach (var item in request.Items) { if (!_inventory.IsInStock(item.ProductId, item.Quantity)) { return Result.Failure($"Product {item.ProductId} is out of stock."); } } return CallNext(request); }} public class PaymentValidationHandler : OrderValidationHandler{ public override Result Validate(CreateOrderRequest request) { if (string.IsNullOrWhiteSpace(request.PaymentMethodId)) { return Result.Failure("Payment method is required."); } return CallNext(request); }} public class FraudCheckHandler : OrderValidationHandler{ private readonly IFraudService _fraudService; public FraudCheckHandler(IFraudService fraudService) { _fraudService = fraudService; } public override Result Validate(CreateOrderRequest request) { if (_fraudService.IsSuspicious(request)) { return Result.Failure("Order flagged for review."); } return CallNext(request); }}
Wire it up:
var stockHandler = new StockAvailabilityHandler(inventoryService); stockHandler .SetNext(new PaymentValidationHandler()) .SetNext(new FraudCheckHandler(fraudService)); Result result = stockHandler.Validate(orderRequest);
Each handler has a single responsibility. Adding a new validation step (like address verification) means creating one new class and linking it into the chain. Zero changes to existing handlers.
Compare this to a single method with 5 nested if statements and multiple service dependencies. The chain is easier to test, easier to extend, and easier to read.
The Chain of Responsibility is a behavioral design pattern where a request is passed along a chain of handlers. Each handler decides whether to process the request or forward it to the next handler. This decouples the sender from the receiver and allows multiple objects to handle the request without the sender knowing which one will.
ASP.NET Core middleware is a direct implementation of this pattern. Each middleware component in the HTTP pipeline receives a request, optionally processes it, and calls next() to pass it to the next middleware. Authentication, CORS, routing, and exception handling all work this way.
Use Chain of Responsibility when you have more than 3-4 conditions that are likely to grow over time, when each condition has complex logic that deserves its own class, or when you need to add, remove, or reorder rules without modifying existing code. For simple, stable conditions, if-else is perfectly fine.
Yes. Register each handler in the DI container and resolve them in your composition root. You can also use a factory that builds the chain based on configuration. This lets handlers have their own dependencies (like services or repositories) injected through the constructor.
The Strategy pattern selects one algorithm from a set and executes it. The Chain of Responsibility passes a request through multiple handlers sequentially, where each handler can process, modify, or skip the request. Strategy is "pick one", Chain of Responsibility is "try each in order".
For more design patterns, check out the Strategy Pattern in .NET and the Adapter Pattern in .NET.
The Chain of Responsibility pattern turns tangled if-else logic into a clean, extensible pipeline of handlers.
Each handler does one thing. You can add, remove, or reorder steps without touching existing code. And if you have ever used ASP.NET Core middleware, you have already been using this pattern.
Start simple: identify a method with multiple branching conditions, extract each branch into a handler, and link them into a chain. The code becomes easier to test, easier to maintain, and easier to explain.
That's all from me today.
P.S. Follow me on YouTube.
Want more design patterns with real-world examples? My ebook Design Patterns that Deliver covers 5 essential patterns (Builder, Decorator, Strategy, Adapter, Mediator) with hands-on C# code you can use right away. Or try a free chapter on the Builder Pattern first.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.