When to Use Chain of Responsibility Pattern in C#: Decision Guide with Examples
You've learned what the chain of responsibility pattern is. You can sketch the class diagram and write a basic handler chain. But the real question is: when to use the chain of responsibility pattern in C# in production code? Picking the right pattern for the job separates thoughtful design from over-engineering -- and this guide is here to help you make that call with confidence.
The chain of responsibility pattern shines in specific situations, and it can be dead weight in others. In this article, we'll walk through the decision criteria that tell you whether a handler chain is the right fit, explore three practical scenarios with C# code examples, and cover the situations where you should reach for a different pattern entirely. By the end, you'll have a decision framework you can apply the next time you're staring at a design choice.
Decision Criteria: When Does Chain of Responsibility Fit?
Before diving into scenarios, let's establish the conditions where a chain of responsibility pattern earns its place. Not every request-handling problem calls for a chain -- but when several of these criteria line up, you're likely looking at a strong candidate.
Multiple potential handlers for the same request. If you have several objects that could handle a given request and you don't want the sender to know which one actually does, a chain lets you decouple that decision. The sender fires the request into the chain, and the chain figures out who deals with it.
The handler set or order needs to change at runtime. When you need to add, remove, or reorder handlers without modifying the calling code, the chain of responsibility pattern gives you that flexibility. You can wire up handlers through dependency injection with IServiceCollection and swap configurations without touching the core logic.
You want to decouple senders from receivers. This is the classic motivation. The sender doesn't know -- and shouldn't care -- which handler processes the request. That decoupling makes your code easier to extend and test.
There are a couple more signals worth watching for. If your processing follows a pipeline or filter pattern -- where a request flows through multiple stages, each deciding whether to act or pass along -- that's a natural fit. And if you find yourself writing long if-else chains or switch statements that dispatch requests to different processors, a chain of responsibility can clean that up considerably.
When you start seeing two or three of these criteria in the same problem, it's worth investing in the pattern. Let's look at what that looks like in practice.
Scenario: Request Validation Pipeline
One of the most common places where the chain of responsibility pattern fits naturally is a request validation pipeline. Think about an API endpoint that needs to validate incoming requests across multiple concerns: format, authentication, authorization, and business rules. Each concern is independent, and you want the ability to plug in or remove validators without touching the others.
Here's how you might structure this in C#:
public abstract class RequestValidator
{
private RequestValidator? _next;
public RequestValidator SetNext(RequestValidator next)
{
_next = next;
return next;
}
public virtual ValidationResult Handle(ApiRequest request)
{
if (_next is not null)
{
return _next.Handle(request);
}
return ValidationResult.Success();
}
}
Each concrete validator checks its own concern and either rejects the request or passes it down the chain. Here's what the format validator looks like:
public sealed class FormatValidator : RequestValidator
{
public override ValidationResult Handle(ApiRequest request)
{
if (string.IsNullOrWhiteSpace(request.Body))
{
return ValidationResult.Failure(
"Request body cannot be empty.");
}
if (!request.ContentType.Equals(
"application/json",
StringComparison.OrdinalIgnoreCase))
{
return ValidationResult.Failure(
"Content type must be application/json.");
}
// Format is valid -- pass to next handler
return base.Handle(request);
}
}
The authentication and authorization validators follow the same shape:
public sealed class AuthenticationValidator : RequestValidator
{
private readonly ITokenService _tokenService;
public AuthenticationValidator(ITokenService tokenService)
{
_tokenService = tokenService;
}
public override ValidationResult Handle(ApiRequest request)
{
if (string.IsNullOrWhiteSpace(request.AuthToken))
{
return ValidationResult.Failure(
"Authentication token is required.");
}
if (!_tokenService.IsValid(request.AuthToken))
{
return ValidationResult.Failure(
"Invalid authentication token.");
}
return base.Handle(request);
}
}
public sealed class AuthorizationValidator : RequestValidator
{
private readonly IPermissionService _permissionService;
public AuthorizationValidator(
IPermissionService permissionService)
{
_permissionService = permissionService;
}
public override ValidationResult Handle(ApiRequest request)
{
if (!_permissionService.HasAccess(
request.UserId,
request.Resource))
{
return ValidationResult.Failure(
"Insufficient permissions for this resource.");
}
return base.Handle(request);
}
}
Wiring the chain together becomes straightforward:
var formatValidator = new FormatValidator();
var authValidator = new AuthenticationValidator(tokenService);
var authzValidator = new AuthorizationValidator(permissionService);
var rulesValidator = new BusinessRulesValidator(rulesEngine);
formatValidator
.SetNext(authValidator)
.SetNext(authzValidator)
.SetNext(rulesValidator);
ValidationResult result = formatValidator.Handle(incomingRequest);
The beauty here is that each validator is a self-contained class with a single responsibility. You can test each one in isolation, reorder them, or add new validators without modifying existing code. This is exactly the kind of scenario where the chain of responsibility pattern in C# pays dividends. If you're comparing this approach to having a facade that delegates to fixed validation logic, the chain gives you runtime flexibility that a facade alone doesn't.
Scenario: Logging and Audit Chain
Logging is another domain where handler chains fit well. Different log entries need different treatment depending on their severity, and you might want entries to pass through formatters, filters, and output handlers before reaching their destination.
Consider a logging chain where each handler decides whether it should process the log entry or pass it along:
public abstract class LogHandler
{
private LogHandler? _next;
public LogHandler SetNext(LogHandler next)
{
_next = next;
return next;
}
public virtual void Handle(LogEntry entry)
{
_next?.Handle(entry);
}
}
public sealed class ConsoleLogHandler : LogHandler
{
private readonly LogLevel _minimumLevel;
public ConsoleLogHandler(LogLevel minimumLevel)
{
_minimumLevel = minimumLevel;
}
public override void Handle(LogEntry entry)
{
if (entry.Level >= _minimumLevel)
{
Console.WriteLine(
$"[{entry.Level}] {entry.Timestamp}: {entry.Message}");
}
// Always pass to next handler
base.Handle(entry);
}
}
Now you can add specialized handlers for different output targets:
public sealed class FileLogHandler : LogHandler
{
private readonly string _filePath;
private readonly LogLevel _minimumLevel;
public FileLogHandler(string filePath, LogLevel minimumLevel)
{
_filePath = filePath;
_minimumLevel = minimumLevel;
}
public override void Handle(LogEntry entry)
{
if (entry.Level >= _minimumLevel)
{
File.AppendAllText(
_filePath,
$"[{entry.Level}] {entry.Timestamp}: " +
$"{entry.Message}{Environment.NewLine}");
}
base.Handle(entry);
}
}
public sealed class AlertLogHandler : LogHandler
{
private readonly IAlertService _alertService;
public AlertLogHandler(IAlertService alertService)
{
_alertService = alertService;
}
public override void Handle(LogEntry entry)
{
if (entry.Level >= LogLevel.Critical)
{
_alertService.SendAlert(
$"Critical: {entry.Message}");
}
base.Handle(entry);
}
}
Notice something different about this chain compared to the validation example. In the validation pipeline, processing stops when a handler rejects the request. In this logging chain, every handler gets a chance to process the entry. Both are valid uses of the chain of responsibility pattern -- the key difference is whether handlers short-circuit the chain or always pass it along.
Wiring this chain lets you control routing by log level without complex conditional logic:
var consoleHandler = new ConsoleLogHandler(LogLevel.Debug);
var fileHandler = new FileLogHandler("app.log", LogLevel.Warning);
var alertHandler = new AlertLogHandler(alertService);
consoleHandler
.SetNext(fileHandler)
.SetNext(alertHandler);
consoleHandler.Handle(new LogEntry(
LogLevel.Critical,
DateTimeOffset.UtcNow,
"Database connection pool exhausted"));
Debug messages go to the console. Warnings and above get written to a file. Critical entries trigger an alert. The sender doesn't need to know any of this -- it just hands the entry to the first handler. This is where understanding when to use the chain of responsibility pattern in C# really matters, because the alternative is a mess of if statements scattered through your logging infrastructure.
Scenario: Exception Handling Chain
Exception handling is a scenario where the chain of responsibility pattern can bring a lot of clarity. Different exception types often need different recovery strategies, and a handler chain lets you organize that logic cleanly without a giant catch block full of if checks.
Here's a structure where each handler specializes in a particular exception category:
public abstract class ExceptionHandler
{
private ExceptionHandler? _next;
public ExceptionHandler SetNext(ExceptionHandler next)
{
_next = next;
return next;
}
public virtual ExceptionResult Handle(Exception exception)
{
if (_next is not null)
{
return _next.Handle(exception);
}
// No handler could process the exception
return ExceptionResult.Unhandled(exception);
}
}
public sealed class DatabaseExceptionHandler : ExceptionHandler
{
private readonly ILogger _logger;
public DatabaseExceptionHandler(ILogger logger)
{
_logger = logger;
}
public override ExceptionResult Handle(Exception exception)
{
if (exception is DbException dbEx)
{
_logger.LogError(
dbEx,
"Database error encountered");
return ExceptionResult.Handled(
"A database error occurred. " +
"Please try again later.",
503);
}
return base.Handle(exception);
}
}
You can add handlers for network issues and validation failures with the same approach:
public sealed class NetworkExceptionHandler : ExceptionHandler
{
private readonly ILogger _logger;
private readonly ICircuitBreaker _circuitBreaker;
public NetworkExceptionHandler(
ILogger logger,
ICircuitBreaker circuitBreaker)
{
_logger = logger;
_circuitBreaker = circuitBreaker;
}
public override ExceptionResult Handle(Exception exception)
{
if (exception is HttpRequestException httpEx)
{
_logger.LogWarning(
httpEx,
"Network error during external call");
_circuitBreaker.RecordFailure();
return ExceptionResult.Handled(
"An external service is unavailable. " +
"Please try again shortly.",
502);
}
return base.Handle(exception);
}
}
public sealed class ValidationExceptionHandler : ExceptionHandler
{
public override ExceptionResult Handle(Exception exception)
{
if (exception is ValidationException valEx)
{
return ExceptionResult.Handled(
valEx.Message,
400);
}
return base.Handle(exception);
}
}
Each handler checks whether it can deal with the exception and either returns a result or passes it to the next handler. The state pattern might also come to mind for managing error states, but the chain of responsibility gives you more flexibility when the exception types aren't known at compile time and handlers need to be composed dynamically.
The chain gets assembled and used like this:
var dbHandler = new DatabaseExceptionHandler(logger);
var networkHandler = new NetworkExceptionHandler(
logger,
circuitBreaker);
var validationHandler = new ValidationExceptionHandler();
dbHandler
.SetNext(networkHandler)
.SetNext(validationHandler);
try
{
await ProcessOrderAsync(order);
}
catch (Exception ex)
{
ExceptionResult result = dbHandler.Handle(ex);
if (!result.WasHandled)
{
throw; // Re-throw if no handler claimed it
}
}
This keeps your catch blocks thin and your exception handling logic organized. Adding a handler for a new exception category means writing one class and plugging it into the chain -- no modifications to existing handlers required.
When NOT to Use Chain of Responsibility
Knowing when to use the chain of responsibility pattern in C# is just as much about knowing when to avoid it. Here are the situations where a different pattern serves you better.
If every handler must process the request, the chain of responsibility pattern isn't the best fit. The pattern is designed around the idea that handlers can choose whether to handle or pass along. If every handler must participate, you're looking at the observer pattern instead, where all subscribers are notified and each one acts independently.
If you know exactly which handler to use upfront, there's no need for a chain. When the selection logic is deterministic based on some input, the strategy pattern gives you a cleaner approach. You select the right strategy and call it directly -- no chain traversal required.
If the processing sequence is fixed and every step runs in order, the template method pattern is a better choice. Template method defines a fixed algorithm skeleton with customizable steps. Chain of responsibility, by contrast, is about dynamic dispatch where handlers can short-circuit.
There's one more scenario worth considering. If the chain adds indirection for logic that's genuinely simple -- say, two or three conditions that aren't going to change -- a chain of responsibility is overkill. A simple if-else or switch statement is easier to read, easier to debug, and easier for the next developer to understand. Don't reach for a pattern just because it exists. Sometimes the command pattern might be tempting for encapsulating requests, but if you don't need the chain's dynamic handler resolution, you're adding complexity without benefit.
Decision Flowchart: Choosing the Right Pattern
When you're deciding between the chain of responsibility pattern and similar behavioral patterns, walk through these questions:
1. Does the request need to be processed by exactly one handler, but you don't know which one at compile time? If yes -- consider chain of responsibility. The chain lets each handler inspect the request and decide whether to claim it.
2. Do you know which handler to use based on some input value or type? If yes -- use the strategy pattern. Direct selection beats chain traversal when the mapping is clear.
3. Should all handlers be notified and act independently? If yes -- use the observer pattern. Observer is built for broadcast scenarios where multiple parties react to the same event.
4. Is the processing a fixed sequence of steps that always runs in the same order? If yes -- use the template method pattern. Template method enforces a consistent algorithm while letting you customize individual steps.
5. Do you need to queue, undo, or log requests as first-class objects? If yes -- use the command pattern. Command encapsulates requests as objects, giving you undo/redo and audit capabilities that chain of responsibility doesn't provide.
When multiple criteria overlap -- for example, you want dynamic handler selection AND the ability to queue requests -- you might combine patterns. But start with the one that addresses your primary concern and layer complexity only when it's justified.
Wrapping Up
The chain of responsibility pattern in C# is a powerful tool for scenarios involving dynamic handler resolution, configurable pipelines, and sender-receiver decoupling. But like any design pattern, it earns its place by solving a specific set of problems -- not by being clever. The decision criteria, scenarios, and flowchart in this article should give you a practical framework for evaluating when to use the chain of responsibility pattern in C# the next time you face a design decision. Start simple, reach for patterns when the problem demands them, and always weigh the cost of indirection against the flexibility it provides.
Frequently Asked Questions
How does the chain of responsibility pattern differ from middleware in ASP.NET Core?
ASP.NET Core middleware is essentially an implementation of the chain of responsibility pattern. Each middleware component decides whether to handle the request, modify it, or pass it to the next component in the pipeline. The primary difference is that ASP.NET Core provides the infrastructure -- you're working within a framework-managed chain rather than building your own from scratch.
Can the chain of responsibility pattern handle async operations in C#?
Yes. You can make the Handle method return a Task<T> or ValueTask<T> and use async/await throughout the chain. Each handler awaits the result from the next handler in the chain, keeping the asynchronous flow intact. This is common in validation pipelines and HTTP processing chains.
What happens if no handler in the chain processes the request?
That depends on your design. You can have the base handler return a default result, throw an exception, or return a "not handled" indicator. In the exception handling scenario above, the chain returns an ExceptionResult.Unhandled value, letting the caller decide what to do. This is a design choice -- not a limitation of the pattern.
Is the chain of responsibility pattern the same as the decorator pattern?
They're structurally similar -- both involve chaining objects -- but they serve different purposes. The decorator pattern wraps an object to add behavior while preserving the interface. The chain of responsibility pattern routes a request through handlers where each one decides whether to handle or pass. Decorators always delegate to the wrapped object; chain handlers might stop processing entirely.
Should I use an abstract class or an interface for the handler base?
Either works, but an abstract class is convenient when you want a default Handle implementation that passes to the next handler. An interface gives you more flexibility if handlers need to inherit from other classes. In C#, default interface methods offer a middle ground, but an abstract class is the more common approach for chain of responsibility implementations.
How do I test individual handlers in a chain of responsibility?
Test each handler in isolation by setting up the handler without a next handler (or with a mock next handler). Verify that it handles requests it should claim, passes requests it shouldn't, and interacts correctly with its dependencies. You can also write integration tests that assemble a full chain to verify end-to-end behavior.
When should I use chain of responsibility instead of a simple dictionary lookup?
If your handler selection logic is a straightforward key-to-handler mapping, a dictionary is simpler and faster. Use chain of responsibility when the decision about which handler should process the request involves inspecting the request itself, when multiple handlers might partially process the request, or when the handler set needs to be composed dynamically at runtime. If a Dictionary<Type, IHandler> solves your problem, you probably don't need a chain.

