Chain of Responsibility Pattern
Pass a request along handlers until one accepts it, with an explicit unhandled result.
· Last updated: 2026-10-25 · 6 min read
Overview
Billing and technical questions have different handlers. Rather than making the intake screen know every handler rule, send the question through an ordered set of handlers.
Chain of Responsibility lets a handler accept a request or pass it to the next one. This example stops at the first match and returns an unhandled result if the chain ends without one.
UML
The caller uses the first Handler. Each handler can refer to a successor. BillingHandler and TechHandler recognize their own ticket kinds; the calling code assembles their order.
History
GoF cataloged Chain of Responsibility as a behavioral pattern in Design Patterns in 1994. A chain avoids binding the sender to one particular receiver.1
Why use it?
The intake screen stays unchanged when handler order or composition changes. Each handler owns its acceptance rule.
This differs from a pipeline that must run every step. Define an unhandled result and decide who communicates it rather than silently dropping the request.
Example
Put the billing handler before the technical handler and send one billing question.
using System;
using System.Collections.Generic;
using System.Linq;
Handler chain = new BillingHandler(new TechHandler(null));
Reply result = chain.Handle(new Ticket("billing"));
Console.WriteLine(result.Handled + ": " + result.Message);
sealed record Ticket(string Kind);
sealed record Reply(bool Handled, string Message);
// Keep acceptance rules in handlers and share the forwarding flow in the base class.
abstract class Handler(Handler? next)
{
public Reply Handle(Ticket ticket)
{
Reply? result = TryHandle(ticket);
if (result is not null) return result;
// null means this handler declines the ticket, so pass it to the next handler.
// If there is no successor, return Handled=false for the whole chain.
return next?.Handle(ticket) ?? new Reply(false, "unhandled: " + ticket.Kind);
}
protected abstract Reply? TryHandle(Ticket ticket);
}
// Decline with null without choosing the next handler here.
sealed class BillingHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "billing" ? new Reply(true, "billing desk") : null;
}
sealed class TechHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "technical" ? new Reply(true, "technical desk") : null;
}
True: billing deskBillingHandler accepts it, so the request does not reach TechHandler.
An extended example
Send a technical ticket and an unsupported ticket. The second returns Handled=false rather than a fabricated success.
using System;
using System.Collections.Generic;
using System.Linq;
Handler chain = new BillingHandler(new TechHandler(null));
foreach (string kind in new[] { "technical", "other" })
{
Reply result = chain.Handle(new Ticket(kind));
Console.WriteLine(result.Handled + ": " + result.Message);
}
sealed record Ticket(string Kind);
sealed record Reply(bool Handled, string Message);
// Keep acceptance rules in handlers and share the forwarding flow in the base class.
abstract class Handler(Handler? next)
{
public Reply Handle(Ticket ticket)
{
Reply? result = TryHandle(ticket);
if (result is not null) return result;
// null means this handler declines the ticket, so pass it to the next handler.
// If there is no successor, return Handled=false for the whole chain.
return next?.Handle(ticket) ?? new Reply(false, "unhandled: " + ticket.Kind);
}
protected abstract Reply? TryHandle(Ticket ticket);
}
// Decline with null without choosing the next handler here.
sealed class BillingHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "billing" ? new Reply(true, "billing desk") : null;
}
sealed class TechHandler(Handler? next) : Handler(next)
{
protected override Reply? TryHandle(Ticket ticket) =>
ticket.Kind == "technical" ? new Reply(true, "technical desk") : null;
}
True: technical desk
False: unhandled: otherOrder matters if several handlers can accept a request. This example has a finite two-step chain; dynamically assembled chains also need protection against cycles.
TypeScript example
Intake code sends a request to the first handler; each handler decides whether to accept it. This separates handler selection from the intake screen, allowing the chain’s order and composition to change.
type Ticket = Readonly<{ kind: string }>;
type Reply = Readonly<{ handled: boolean; message: string }>;
// Keep acceptance rules in handlers and share the forwarding flow in the base class.
abstract class Handler {
constructor(private readonly next: Handler | null) {}
handle(ticket: Ticket): Reply {
const reply = this.tryHandle(ticket);
if (reply !== null) return reply;
return this.next?.handle(ticket) ?? { handled: false, message: "unhandled: " + ticket.kind };
}
protected abstract tryHandle(ticket: Ticket): Reply | null;
}
// Decline with null without choosing the next handler here.
class BillingHandler extends Handler {
protected override tryHandle(ticket: Ticket): Reply | null {
return ticket.kind === "billing" ? { handled: true, message: "billing desk" } : null;
}
}
class TechHandler extends Handler {
protected override tryHandle(ticket: Ticket): Reply | null {
return ticket.kind === "technical" ? { handled: true, message: "technical desk" } : null;
}
}
const chain = new BillingHandler(new TechHandler(null));
for (const kind of ["billing", "technical", "other"]) {
const result = chain.handle({ kind });
console.log(result.handled + ": " + result.message);
}true: billing desk
true: technical desk
false: unhandled: othernull means that the current handler declined; handled=false is the final outcome at the end of the chain. The example stops at the first handler that responds.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Chain of Responsibility. ↩