Skip to content

// Design Patterns · 13

Chain of Responsibility

Request with many checksleads toPass along a handler chain

Pass a request along a chain until a handler deals with it.

How it fits together

Chain of Responsibility structure«client»RefundServiceApprove(refund)AgentApproverup to 50«abstract»RefundApprovernext: RefundApproverHandle(refund)LeadApproverup to 500FinanceApproverup to 10,000RejectedHandle(r)nextnextend of chain
uses / callsextendswraps / holdsfails / error path
  • RefundService uses / calls RefundApprover (Handle(r))
  • AgentApprover extends RefundApprover
  • LeadApprover extends RefundApprover
  • FinanceApprover extends RefundApprover
  • AgentApprover wraps / holds LeadApprover (next)
  • LeadApprover wraps / holds FinanceApprover (next)
  • FinanceApprover fails / error path Rejected (end of chain)

Spot it

  • A long if/else of checks in one method
  • Checks need reordering or adding per customer
  • The sender should not know who handles it

It costs

No guarantee anyone handles it, and the flow spreads over many classes.

In code

public abstract class RefundApprover{    private RefundApprover? _next;​    public RefundApprover SetNext(RefundApprover next) => _next = next;​    public virtual Decision Handle(Refund refund) =>        _next?.Handle(refund) ?? Decision.Rejected("No approver for this amount");}​public sealed class AgentApprover : RefundApprover{    public override Decision Handle(Refund refund) =>        refund.Amount <= 50m ? Decision.Approved("agent") : base.Handle(refund);}​public static class RefundChain{    public static RefundApprover Build()    {        var first = new AgentApprover();        first            .SetNext(new LeadApprover())            .SetNext(new FinanceApprover());        return first;    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Each handler holds a reference to the next one. It either handles the request and stops, or passes it on. The sender only knows the first link, so handlers can be added, removed or reordered without touching it.

Always define what happens at the end of the chain, such as a terminal step that rejects or logs, or requests vanish silently. Build the order explicitly in one place, and avoid handlers that rely on what earlier ones did to shared state.

HTTP middleware pipelines are the common modern form, where each handler may act and then call next. Decorator has the same linked shape, but every wrapper always delegates. A Command is often the request object that travels down the chain.

Ask for it

  • handler
  • successor (next)
  • chain
  • short-circuit
  • pipeline
  • middleware

ApproveRefund has one method with limits for agents, team leads and finance. Split it into RefundApprover handlers (AgentApprover up to 50, LeadApprover up to 500, FinanceApprover up to 10,000), each holding next, built in that order at startup. Anything left over is rejected, never dropped.