Chain of Responsibility vs Decorator Pattern in C#: Key Differences Explained
If you've worked with design patterns for a while, you've probably noticed that the chain of responsibility and the decorator pattern share a suspicious amount of DNA. Both involve linking objects together. Both let you compose behavior without modifying existing classes. Both rely on an inner reference that forwards work down the line. But the similarities end once you look at what each pattern actually does with that chain. Understanding chain of responsibility vs decorator pattern in C# is the key to picking the right tool when your design calls for linked objects -- because choosing wrong leads to code that fights against its own structure.
The chain of responsibility routes a request through a sequence of handlers where any handler can stop processing. The decorator wraps a single operation with layered behavior where every layer always executes. That distinction -- short-circuiting versus guaranteed execution -- shapes everything from how you register components in your IServiceCollection to how you reason about failures. In this article, we'll break down each pattern individually, compare them side by side against the same scenario, and explore a practical case where both patterns work together.
Chain of Responsibility: Pass Until Handled
The chain of responsibility pattern organizes a set of handler objects into a linear sequence. When a request enters the chain, the first handler inspects it. That handler can process the request, pass it to the next handler, or do both. The critical feature is that any handler in the chain can stop propagation entirely -- once a handler decides it owns the request, the remaining handlers never see it.
This makes the chain of responsibility ideal for scenarios where a request has multiple potential owners and only one of them should act. Think of it like an escalation path in a support system: the front-line agent tries first, then a specialist, then a manager. Once someone resolves the ticket, the chain stops.
Key Characteristics
Three traits define the chain of responsibility pattern and separate it from other compositional patterns:
- Short-circuiting -- a handler can consume the request and prevent further processing.
- Optional participation -- each handler independently decides whether to act or pass.
- Loose coupling -- the sender doesn't know which handler will ultimately process the request.
These characteristics make the pattern especially useful for validation pipelines, authorization checks, and request routing. The Command Design Pattern in C# also deals with encapsulating requests, but it focuses on packaging operations for execution rather than routing them through a chain of potential handlers.
C# Code Example: Approval Workflow Chain
Consider an expense approval workflow where different managers have different spending limits. Each handler checks whether it can approve the amount, and if not, it passes the request up the chain:
public record ExpenseRequest(
string Description,
decimal Amount);
public abstract class ApprovalHandler
{
private ApprovalHandler? _nextHandler;
public ApprovalHandler SetNext(ApprovalHandler next)
{
_nextHandler = next;
return next;
}
public virtual string Handle(ExpenseRequest request)
{
if (_nextHandler is not null)
{
return _nextHandler.Handle(request);
}
return $"No one approved: {request.Description}";
}
}
public class TeamLeadApproval : ApprovalHandler
{
public override string Handle(ExpenseRequest request)
{
if (request.Amount <= 1000m)
{
return $"Team Lead approved: {request.Description}";
}
return base.Handle(request);
}
}
public class DirectorApproval : ApprovalHandler
{
public override string Handle(ExpenseRequest request)
{
if (request.Amount <= 10_000m)
{
return $"Director approved: {request.Description}";
}
return base.Handle(request);
}
}
public class VPApproval : ApprovalHandler
{
public override string Handle(ExpenseRequest request)
{
if (request.Amount <= 100_000m)
{
return $"VP approved: {request.Description}";
}
return base.Handle(request);
}
}
Setting up the chain links these handlers in priority order:
var teamLead = new TeamLeadApproval();
var director = new DirectorApproval();
var vp = new VPApproval();
teamLead.SetNext(director);
director.SetNext(vp);
// A $500 request stops at TeamLead
var result1 = teamLead.Handle(new ExpenseRequest("Office supplies", 500m));
// A $5,000 request skips TeamLead, stops at Director
var result2 = teamLead.Handle(new ExpenseRequest("Conference tickets", 5000m));
// A $50,000 request goes all the way to VP
var result3 = teamLead.Handle(new ExpenseRequest("Team offsite", 50_000m));
Notice how the chain short-circuits. The $500 request never reaches the Director or VP. Each handler either owns the request or explicitly passes it along. This is the chain of responsibility vs decorator pattern distinction in action -- a decorator would never skip a layer.
Decorator: Wrap and Enhance
The decorator pattern takes a fundamentally different approach to composition. Instead of routing a request to one of many possible handlers, the decorator wraps a single operation and guarantees that every layer in the stack executes. Each decorator implements the same interface as the object it wraps, adds behavior before or after the inner call, and always delegates to the wrapped object.
This makes the decorator pattern the right choice for cross-cutting concerns -- logging, caching, validation, retry logic, metrics -- where you want to augment an operation without changing its core behavior. The Decorator Design Pattern in C# goes into much greater depth on implementation techniques and registration patterns.
Key Characteristics
Three traits define the decorator pattern:
- No short-circuiting -- every decorator in the stack always executes.
- Same interface -- each decorator implements the exact same interface as the wrapped object.
- Transparent composition -- the client has no idea how many decorators are in the stack or what they do.
The decorator is all about enhancement, not routing. If you strip away every decorator, the core operation still runs. The decorators simply add value around it. This is similar to how the Proxy Design Pattern in C# wraps an object behind the same interface, though proxies focus on access control rather than behavior enhancement.
C# Code Example: Expense Processing with Decorators
Let's model the same expense approval domain, but this time using decorators. Instead of routing the request to the right approver, we'll process an already-approved expense through a pipeline of cross-cutting behaviors:
public interface IExpenseProcessor
{
string Process(ExpenseRequest request);
}
public class CoreExpenseProcessor : IExpenseProcessor
{
public string Process(ExpenseRequest request)
{
return $"Processed expense: {request.Description} " +
$"for {request.Amount:C}";
}
}
public class LoggingExpenseDecorator : IExpenseProcessor
{
private readonly IExpenseProcessor _inner;
public LoggingExpenseDecorator(IExpenseProcessor inner)
{
_inner = inner;
}
public string Process(ExpenseRequest request)
{
Console.WriteLine(
$"[LOG] Processing expense: {request.Description}");
var result = _inner.Process(request);
Console.WriteLine($"[LOG] Result: {result}");
return result;
}
}
public class ValidationExpenseDecorator : IExpenseProcessor
{
private readonly IExpenseProcessor _inner;
public ValidationExpenseDecorator(IExpenseProcessor inner)
{
_inner = inner;
}
public string Process(ExpenseRequest request)
{
if (request.Amount <= 0)
{
throw new ArgumentException(
"Expense amount must be positive.");
}
if (string.IsNullOrWhiteSpace(request.Description))
{
throw new ArgumentException(
"Expense description is required.");
}
return _inner.Process(request);
}
}
public class AuditExpenseDecorator : IExpenseProcessor
{
private readonly IExpenseProcessor _inner;
public AuditExpenseDecorator(IExpenseProcessor inner)
{
_inner = inner;
}
public string Process(ExpenseRequest request)
{
var result = _inner.Process(request);
Console.WriteLine(
$"[AUDIT] Expense '{request.Description}' " +
$"({request.Amount:C}) recorded for audit.");
return result;
}
}
Composing the decorators wraps each layer around the previous one:
IExpenseProcessor processor = new CoreExpenseProcessor();
processor = new ValidationExpenseDecorator(processor);
processor = new LoggingExpenseDecorator(processor);
processor = new AuditExpenseDecorator(processor);
// Every decorator executes for every request
var result = processor.Process(
new ExpenseRequest("Team lunch", 250m));
Every single decorator runs for every request. The validation decorator checks the input. The logging decorator records the operation. The audit decorator logs the result. None of them can skip the inner processor or prevent other decorators from executing. This is the core structural difference when comparing chain of responsibility vs decorator pattern in C# -- decorators compose behavior; chains route decisions.
Side-by-Side Comparison
Looking at the same expense domain through both patterns reveals their different philosophies. The chain of responsibility answers "who should handle this?" while the decorator answers "what should happen around this?" Here's how they compare across the dimensions that matter for design decisions:
| Dimension | Chain of Responsibility | Decorator |
|---|---|---|
| Short-circuiting | Yes -- any handler can stop the chain | No -- every layer always executes |
| All layers execute | Optional -- depends on handler logic | Always -- guaranteed full stack execution |
| Interface contract | May use abstract base class or interface | Strictly same interface throughout |
| Linking mechanism | Explicit next-handler chain | Nested constructor wrapping |
| Primary use case | Routing, filtering, escalation | Enhancement, cross-cutting concerns |
| Order sensitivity | Determines which handler gets first shot | Determines execution sequence |
The chain of responsibility and the decorator pattern also differ in how they handle the concept of "failure." In a chain, if no handler processes the request, the chain falls through -- that's a routing failure. In a decorator stack, the core operation always runs; a decorator failure means something went wrong in the enhancement layer, not in the routing.
Understanding these tradeoffs is important for choosing the right pattern. Both involve composition and forwarding, and the Strategy Design Pattern in C# offers yet another angle on behavioral flexibility -- swapping entire algorithms rather than chaining or wrapping them.
When to Choose Chain of Responsibility
The chain of responsibility pattern earns its place when your problem is fundamentally about routing a request to the right handler. If you find yourself writing conditional logic that asks "should I handle this or pass it along?" then a chain is probably the right fit.
Here are the scenarios where chain of responsibility shines. First, consider request classification -- when a request could be handled by one of several processors and you need a clean way to determine which one takes ownership. Middleware pipelines in web frameworks are a classic example: each middleware checks whether it should handle the request and either responds or calls next().
Second, think about validation with early exit. If your validation logic needs to reject a request at the first failure without running subsequent checks, a chain gives you natural short-circuiting. Each validator either passes the request forward or returns an error.
Third, the pattern works well for escalation workflows like the approval example above. When responsibility cascades through levels of authority or specificity, the chain models that hierarchy directly.
A key signal that you should reach for the chain of responsibility is when you ask: "Can I stop processing early?" If the answer is yes, and different objects own different stopping conditions, the chain of responsibility pattern fits naturally. The Observer Design Pattern in C# also deals with notifying multiple objects, but observers all receive the notification -- there's no short-circuiting -- making it structurally closer to the decorator in that regard.
When to Choose Decorator
The decorator pattern earns its place when every enhancement must execute regardless of what the other enhancements do. If you're adding behavior that should be invisible to both the core operation and the other decorators, wrapping is the right approach.
Start with cross-cutting concerns. Logging, timing, metrics collection, and retry logic are textbook decorator scenarios. You want these behaviors to fire on every call, and you want to be able to add or remove them independently. A logging decorator doesn't care whether a caching decorator also wraps the same service.
Next, consider incremental feature composition. When you need to add capabilities to a service without modifying it -- especially when different configurations require different combinations of capabilities -- decorators let you build exactly the combination you need. Wire up validation and logging in production, strip them for unit tests, add caching in read-heavy scenarios.
Finally, decorators shine when you need interface-preserving enhancement. Because each decorator exposes the same interface, the consuming code never changes. You can add five decorators or remove three, and the call site stays identical. This is especially powerful when using dependency injection with IServiceCollection -- you register decorators at the composition root and the rest of your application stays untouched.
The signal for decorator over chain of responsibility: "Should every layer run?" If yes, decorate. If some layers should be skipped, chain.
Using Both Together
The chain of responsibility and decorator patterns aren't mutually exclusive. In practice, they complement each other well when your system needs both routing and enhancement. A common architecture uses the chain of responsibility to select the right handler and decorators to add cross-cutting behavior around whatever handler gets selected.
Consider an expense system that routes approvals through a chain but wraps the entire approval process with logging, validation, and auditing. The chain handles the "who approves this?" question while the decorators handle the "what else should happen?" question:
public interface IApprovalService
{
string Approve(ExpenseRequest request);
}
public class ChainBasedApprovalService : IApprovalService
{
private readonly ApprovalHandler _chain;
public ChainBasedApprovalService()
{
var teamLead = new TeamLeadApproval();
var director = new DirectorApproval();
var vp = new VPApproval();
teamLead.SetNext(director);
director.SetNext(vp);
_chain = teamLead;
}
public string Approve(ExpenseRequest request)
{
return _chain.Handle(request);
}
}
public class LoggingApprovalDecorator : IApprovalService
{
private readonly IApprovalService _inner;
public LoggingApprovalDecorator(IApprovalService inner)
{
_inner = inner;
}
public string Approve(ExpenseRequest request)
{
Console.WriteLine(
$"[LOG] Approval requested: {request.Description} " +
$"for {request.Amount:C}");
var result = _inner.Approve(request);
Console.WriteLine($"[LOG] Approval result: {result}");
return result;
}
}
public class TimingApprovalDecorator : IApprovalService
{
private readonly IApprovalService _inner;
public TimingApprovalDecorator(IApprovalService inner)
{
_inner = inner;
}
public string Approve(ExpenseRequest request)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var result = _inner.Approve(request);
stopwatch.Stop();
Console.WriteLine(
$"[TIMING] Approval took " +
$"{stopwatch.ElapsedMilliseconds}ms");
return result;
}
}
Wiring the two patterns together is straightforward -- the chain lives inside a service, and decorators wrap that service:
IApprovalService service = new ChainBasedApprovalService();
service = new LoggingApprovalDecorator(service);
service = new TimingApprovalDecorator(service);
// The chain routes to the right approver
// The decorators add logging and timing around it
var result = service.Approve(
new ExpenseRequest("New monitors", 3500m));
The chain of responsibility handles the routing logic internally while the decorators handle everything that should happen regardless of which handler processes the request. This layered approach keeps each pattern focused on what it does best, and it scales cleanly -- you can add new handlers to the chain or new decorators to the wrapper stack without touching existing code. The Adapter Design Pattern in C# can also play a supporting role in these architectures when you need to bridge between incompatible handler interfaces.
Wrapping Up
The chain of responsibility vs decorator pattern in C# comparison boils down to one question: do you need to route or enhance? If your design requires finding the right handler for a request and stopping once it's found, the chain of responsibility gives you clean short-circuiting with loosely coupled handlers. If your design requires adding behavior around an existing operation where every layer must execute, the decorator gives you transparent, composable enhancement.
Both patterns rely on object composition and forwarding, which is why they look similar at first glance. But their intent, execution guarantees, and use cases are different enough that picking the wrong one creates friction in your codebase. And as the combined example shows, you don't always have to pick just one -- using both patterns together lets you build systems that route intelligently and enhance consistently.
Frequently Asked Questions
Can chain of responsibility and decorator be used together in C#?
Yes, and it's a common and effective combination. Use the chain of responsibility inside a service to route requests to the right handler, then wrap that service with decorators for cross-cutting concerns like logging, timing, and validation. The chain handles "who processes this?" while the decorators handle "what else should happen?" -- keeping each pattern focused on its strength.
What is the main difference between chain of responsibility and decorator?
The main difference is short-circuiting. In the chain of responsibility, any handler can stop processing and prevent subsequent handlers from seeing the request. In the decorator pattern, every layer in the stack always executes. The chain routes to one handler; the decorator enhances an operation through all layers.
When should I use chain of responsibility instead of decorator in C#?
Use the chain of responsibility when your problem involves selecting which object should handle a request. If you have multiple potential handlers and only one should act -- like approval escalation, middleware pipelines, or request classification -- the chain of responsibility's short-circuiting behavior models that cleanly. If every handler should always run, you want a decorator instead.
Does the decorator pattern support short-circuiting?
Not by design. The decorator pattern expects every decorator in the stack to delegate to the inner object. If a decorator needs to conditionally skip the inner call -- for example, returning a cached result without hitting the real service -- it's behaving more like a proxy than a decorator. The distinction matters because other decorators in the stack may depend on the inner call executing.
How do I register chain of responsibility handlers in dependency injection?
You can register each handler individually and wire the chain manually in a factory method, or you can use a loop that iterates through registered handler types and links them via SetNext. Some developers prefer wrapping the chain behind a service interface -- like the ChainBasedApprovalService example above -- and registering that service with IServiceCollection. This keeps the chain construction isolated from the rest of the application.
Is middleware in ASP.NET Core a chain of responsibility or decorator?
ASP.NET Core middleware is closer to the chain of responsibility pattern because each middleware can short-circuit the pipeline by not calling next(). However, it borrows from the decorator pattern too -- many middleware components add behavior before and after next() without stopping the chain. In practice, the middleware pipeline is a hybrid that leans toward chain of responsibility for routing and decorator for cross-cutting behavior.
Can the chain of responsibility pattern replace if-else chains in C#?
Yes, and that's one of its most practical benefits. Long if-else or switch chains that route logic to different handlers can be refactored into a chain of responsibility where each handler encapsulates its own condition and logic. This improves testability because each handler can be tested in isolation, and it improves extensibility because you can add new handlers without modifying existing ones.

