Back to Blog
design-patternscsharptypescriptbehavioral-patterns

Chain of Responsibility Pattern — FixIt Pro Series #13

A job approval chain: Handyman → Supervisor → Manager, based on cost threshold. Learn how the Chain of Responsibility pattern passes requests along a chain until one handler deals with it, in C# and TypeScript.


Series: Design Patterns with FixIt Pro  ·  Episode 13 / 22  ·  Behavioral Pattern
Previous: #12 — Proxy Pattern


Welcome to Behavioral Patterns

The first five episodes covered Creational patterns, how objects are created. The next seven covered Structural patterns, how objects are composed. We now move into Behavioral patterns, how objects communicate, distribute responsibility, and coordinate behaviour.

Behavioral patterns are about algorithms and the assignment of responsibilities between objects. They're often the most immediately recognisable patterns in production codebases, you've almost certainly used several without naming them.


The Scenario

FixIt Pro job cards carry a cost estimate. Before a job is dispatched, it needs approval but not every job needs to go to the same person.

The approval rules are:

  • Handyman — can approve jobs up to N$1,000
  • Supervisor — can approve jobs up to N$5,000
  • Manager — can approve jobs up to N$20,000
  • Director — approves anything above N$20,000

A N$750 plumbing fix goes straight to the handyman. A N$3,500 electrical job goes to the supervisor. A N$15,000 renovation goes to the manager. A N$50,000 full refurbishment goes to the director.

The naive approach: a long if/else chain in the dispatcher that checks the cost and routes accordingly. Every time the approval thresholds change, you rewrite the dispatcher. Every time a new role is added, you touch the same file.

The Chain of Responsibility builds an actual chain, each handler decides whether to handle the request or pass it to the next handler in line.


What Is the Chain of Responsibility Pattern?

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

The sender fires the request at the first handler. Each handler either handles it or passes it on. The sender doesn't know who ultimately handles it and doesn't need to.

The three participants

Role FixIt Pro equivalent
Handler interface IApprovalHandler — declares Handle() and SetNext()
Concrete Handlers HandymanApprover, SupervisorApprover, ManagerApprover, DirectorApprover
Client JobApprovalService — builds the chain and fires the request

C# Implementation

// ── Approval Request ───────────────────────────────────────

public class ApprovalRequest
{
    public string  JobId    { get; }
    public string  Title    { get; }
    public decimal Cost     { get; }
    public string  Category { get; }

    public ApprovalRequest(string jobId, string title, decimal cost, string category)
    {
        JobId    = jobId;
        Title    = title;
        Cost     = cost;
        Category = category;
    }

    public override string ToString() =>
        $"Job #{JobId} '{Title}' [{Category}] — N${Cost:N2}";
}

// ── Handler Interface ──────────────────────────────────────

public interface IApprovalHandler
{
    IApprovalHandler SetNext(IApprovalHandler next);
    void             Handle(ApprovalRequest request);
}

// ── Base Handler ───────────────────────────────────────────
// Handles the chain-passing logic so concrete handlers
// only need to implement their own approval logic.

public abstract class BaseApprovalHandler : IApprovalHandler
{
    private IApprovalHandler? _next;

    public IApprovalHandler SetNext(IApprovalHandler next)
    {
        _next = next;
        return next; // return next to enable fluent chaining
    }

    public virtual void Handle(ApprovalRequest request)
    {
        if (_next is not null)
            _next.Handle(request);
        else
            Console.WriteLine($"  ❌ No handler could approve: {request}");
    }
}

// ── Concrete Handlers ──────────────────────────────────────

public class HandymanApprover : BaseApprovalHandler
{
    private const decimal Limit = 1_000m;

    public override void Handle(ApprovalRequest request)
    {
        if (request.Cost <= Limit)
            Console.WriteLine($"  ✅ Handyman approved: {request}");
        else
        {
            Console.WriteLine($"  ↗️  Handyman escalating (N${request.Cost:N2} > N${Limit:N2})...");
            base.Handle(request);
        }
    }
}

public class SupervisorApprover : BaseApprovalHandler
{
    private const decimal Limit = 5_000m;

    public override void Handle(ApprovalRequest request)
    {
        if (request.Cost <= Limit)
            Console.WriteLine($"  ✅ Supervisor approved: {request}");
        else
        {
            Console.WriteLine($"  ↗️  Supervisor escalating (N${request.Cost:N2} > N${Limit:N2})...");
            base.Handle(request);
        }
    }
}

public class ManagerApprover : BaseApprovalHandler
{
    private const decimal Limit = 20_000m;

    public override void Handle(ApprovalRequest request)
    {
        if (request.Cost <= Limit)
            Console.WriteLine($"  ✅ Manager approved: {request}");
        else
        {
            Console.WriteLine($"  ↗️  Manager escalating (N${request.Cost:N2} > N${Limit:N2})...");
            base.Handle(request);
        }
    }
}

public class DirectorApprover : BaseApprovalHandler
{
    public override void Handle(ApprovalRequest request)
    {
        // Director approves everything — no upper limit
        Console.WriteLine($"  ✅ Director approved: {request}");
    }
}

// ── Client — JobApprovalService ────────────────────────────

public class JobApprovalService
{
    private readonly IApprovalHandler _chain;

    public JobApprovalService()
    {
        // Build the chain — fluent SetNext chaining
        var handyman   = new HandymanApprover();
        var supervisor = new SupervisorApprover();
        var manager    = new ManagerApprover();
        var director   = new DirectorApprover();

        handyman
            .SetNext(supervisor)
            .SetNext(manager)
            .SetNext(director);

        _chain = handyman; // entry point
    }

    public void RequestApproval(ApprovalRequest request)
    {
        Console.WriteLine($"\n--- Approval request: {request} ---");
        _chain.Handle(request);
    }
}

// ── Program ────────────────────────────────────────────────

class Program
{
    static void Main()
    {
        var service = new JobApprovalService();

        service.RequestApproval(new ApprovalRequest("JC-401", "Replace tap washer",         750m,    "Plumbing"));
        service.RequestApproval(new ApprovalRequest("JC-402", "Rewire kitchen sockets",     3_200m,  "Electrical"));
        service.RequestApproval(new ApprovalRequest("JC-403", "Full bathroom renovation",   14_500m, "Carpentry"));
        service.RequestApproval(new ApprovalRequest("JC-404", "Office block refurbishment", 55_000m, "General"));
    }
}

Output:

--- Approval request: Job #JC-401 'Replace tap washer' [Plumbing] — N$750.00 ---
  ✅ Handyman approved: Job #JC-401 'Replace tap washer' [Plumbing] — N$750.00

--- Approval request: Job #JC-402 'Rewire kitchen sockets' [Electrical] — N$3,200.00 ---
  ↗️  Handyman escalating (N$3,200.00 > N$1,000.00)...
  ✅ Supervisor approved: Job #JC-402 'Rewire kitchen sockets' [Electrical] — N$3,200.00

--- Approval request: Job #JC-403 'Full bathroom renovation' [Carpentry] — N$14,500.00 ---
  ↗️  Handyman escalating (N$14,500.00 > N$1,000.00)...
  ↗️  Supervisor escalating (N$14,500.00 > N$5,000.00)...
  ✅ Manager approved: Job #JC-403 'Full bathroom renovation' [Carpentry] — N$14,500.00

--- Approval request: Job #JC-404 'Office block refurbishment' [General] — N$55,000.00 ---
  ↗️  Handyman escalating (N$55,000.00 > N$1,000.00)...
  ↗️  Supervisor escalating (N$55,000.00 > N$5,000.00)...
  ↗️  Manager escalating (N$55,000.00 > N$20,000.00)...
  ✅ Director approved: Job #JC-404 'Office block refurbishment' [General] — N$55,000.00

TypeScript Implementation

// ── Model/ApprovalRequest.ts ───────────────────────────────

export class ApprovalRequest {
  constructor(
    readonly jobId:    string,
    readonly title:    string,
    readonly cost:     number,
    readonly category: string
  ) {}

  toString(): string {
    return `Job #${this.jobId} '${this.title}' [${this.category}] — N$${this.cost.toFixed(2)}`;
  }
}

// ── Handler/IApprovalHandler.ts ────────────────────────────

import { ApprovalRequest } from "../Model/ApprovalRequest";

export interface IApprovalHandler {
  setNext(next: IApprovalHandler): IApprovalHandler;
  handle(request: ApprovalRequest): void;
}

// ── Handler/BaseApprovalHandler.ts ────────────────────────

import { IApprovalHandler } from "./IApprovalHandler";
import { ApprovalRequest  } from "../Model/ApprovalRequest";

export abstract class BaseApprovalHandler implements IApprovalHandler {
  private next?: IApprovalHandler;

  setNext(next: IApprovalHandler): IApprovalHandler {
    this.next = next;
    return next; // fluent chaining
  }

  handle(request: ApprovalRequest): void {
    if (this.next)
      this.next.handle(request);
    else
      console.log(`  ❌ No handler could approve: ${request}`);
  }
}

// ── Handler/HandymanApprover.ts ────────────────────────────

import { BaseApprovalHandler } from "./BaseApprovalHandler";
import { ApprovalRequest     } from "../Model/ApprovalRequest";

export class HandymanApprover extends BaseApprovalHandler {
  private static readonly LIMIT = 1_000;

  handle(request: ApprovalRequest): void {
    if (request.cost <= HandymanApprover.LIMIT)
      console.log(`  ✅ Handyman approved: ${request}`);
    else {
      console.log(`  ↗️  Handyman escalating (N$${request.cost.toFixed(2)} > N$${HandymanApprover.LIMIT.toFixed(2)})...`);
      super.handle(request);
    }
  }
}

// ── Handler/SupervisorApprover.ts ──────────────────────────

import { BaseApprovalHandler } from "./BaseApprovalHandler";
import { ApprovalRequest     } from "../Model/ApprovalRequest";

export class SupervisorApprover extends BaseApprovalHandler {
  private static readonly LIMIT = 5_000;

  handle(request: ApprovalRequest): void {
    if (request.cost <= SupervisorApprover.LIMIT)
      console.log(`  ✅ Supervisor approved: ${request}`);
    else {
      console.log(`  ↗️  Supervisor escalating (N$${request.cost.toFixed(2)} > N$${SupervisorApprover.LIMIT.toFixed(2)})...`);
      super.handle(request);
    }
  }
}

// ── Handler/ManagerApprover.ts ─────────────────────────────

import { BaseApprovalHandler } from "./BaseApprovalHandler";
import { ApprovalRequest     } from "../Model/ApprovalRequest";

export class ManagerApprover extends BaseApprovalHandler {
  private static readonly LIMIT = 20_000;

  handle(request: ApprovalRequest): void {
    if (request.cost <= ManagerApprover.LIMIT)
      console.log(`  ✅ Manager approved: ${request}`);
    else {
      console.log(`  ↗️  Manager escalating (N$${request.cost.toFixed(2)} > N$${ManagerApprover.LIMIT.toFixed(2)})...`);
      super.handle(request);
    }
  }
}

// ── Handler/DirectorApprover.ts ────────────────────────────

import { BaseApprovalHandler } from "./BaseApprovalHandler";
import { ApprovalRequest     } from "../Model/ApprovalRequest";

export class DirectorApprover extends BaseApprovalHandler {
  handle(request: ApprovalRequest): void {
    console.log(`  ✅ Director approved: ${request}`);
  }
}

// ── Service/JobApprovalService.ts ──────────────────────────

import { IApprovalHandler    } from "../Handler/IApprovalHandler";
import { HandymanApprover    } from "../Handler/HandymanApprover";
import { SupervisorApprover  } from "../Handler/SupervisorApprover";
import { ManagerApprover     } from "../Handler/ManagerApprover";
import { DirectorApprover    } from "../Handler/DirectorApprover";
import { ApprovalRequest     } from "../Model/ApprovalRequest";

export class JobApprovalService {
  private readonly chain: IApprovalHandler;

  constructor() {
    const handyman   = new HandymanApprover();
    const supervisor = new SupervisorApprover();
    const manager    = new ManagerApprover();
    const director   = new DirectorApprover();

    handyman
      .setNext(supervisor)
      .setNext(manager)
      .setNext(director);

    this.chain = handyman;
  }

  requestApproval(request: ApprovalRequest): void {
    console.log(`\n--- Approval request: ${request} ---`);
    this.chain.handle(request);
  }
}

// ── App.ts ─────────────────────────────────────────────────

import { JobApprovalService } from "./Service/JobApprovalService";
import { ApprovalRequest    } from "./Model/ApprovalRequest";

const service = new JobApprovalService();

service.requestApproval(new ApprovalRequest("JC-401", "Replace tap washer",         750,    "Plumbing"));
service.requestApproval(new ApprovalRequest("JC-402", "Rewire kitchen sockets",     3_200,  "Electrical"));
service.requestApproval(new ApprovalRequest("JC-403", "Full bathroom renovation",   14_500, "Carpentry"));
service.requestApproval(new ApprovalRequest("JC-404", "Office block refurbishment", 55_000, "General"));

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Numeric separators 1_000m, 5_000m, 20_000m 1_000, 5_000, 20_000 (same syntax ✓)
Abstract base class abstract class + virtual void Handle() abstract class + concrete handle()
Null check if (_next is not null) if (this.next)
Pattern matching is not null C# 9 pattern Not applicable
Fluent return Returns IApprovalHandler from SetNext Returns IApprovalHandler from setNext (same)

The numeric separator _ for readability (1_000 instead of 1000) is supported in both languages, a small but useful shared feature worth knowing.


Variations — Pure Chain vs Mixed Chain

The classic Chain of Responsibility stops at the first handler that can process the request. But there are two common variations:

Variation Behaviour Use case
Pure chain First matching handler processes, chain stops Approval workflows, request routing
Mixed chain Every handler processes regardless, chain always continues Logging, event handling, middleware
Modified chain Handler processes AND passes on Audit trail record at every level, approve at the right one

ASP.NET Core middleware is a mixed chain, every middleware in the pipeline runs (unless one short-circuits). FixIt Pro's approval workflow is a pure chain, only one level approves.


When to Use the Chain of Responsibility

Use it when:

  • More than one handler may process a request, and the handler isn't known at design time
  • You want to issue a request to one of several handlers without specifying the receiver explicitly
  • The set of handlers and their order should be configurable dynamically

Avoid it when:

  • Every request must be handled — an unhandled request is a bug in your use case
  • The chain is so long it becomes a performance concern
  • A simple strategy map (Dictionary<condition, handler>) would be cleaner for fixed routing

Real-World Takeaway

The Chain of Responsibility is the pattern behind almost every middleware system in existence. ASP.NET Core's request pipeline, Express.js middleware, NestJS interceptors and guards, Django's middleware stack, all chains. Each middleware decides whether to handle the request, modify it, or pass it on.

In the .NET world, ILogger with multiple providers is a chain, each provider (Console, File, Application Insights) processes the log entry. In UI frameworks, event bubbling in the DOM is a chain, a click event travels from the target element up through its parents until a handler catches it.

In FixIt Pro, the approval chain means adding a new approval tier, say, a Regional Director between Manager and Director is one new class and one new SetNext call. The existing handlers don't change. The dispatcher doesn't change. The chain is just longer.


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Behavioral/13-ChainOfResponsibility/
│   ├── Model/ApprovalRequest.cs
│   ├── Handler/IApprovalHandler.cs
│   ├── Handler/BaseApprovalHandler.cs
│   ├── Handler/HandymanApprover.cs
│   ├── Handler/SupervisorApprover.cs
│   ├── Handler/ManagerApprover.cs
│   ├── Handler/DirectorApprover.cs
│   ├── Service/JobApprovalService.cs
│   └── Program.cs
└── typescript/Behavioral/13-ChainOfResponsibility/
    ├── Model/ApprovalRequest.ts
    ├── Handler/IApprovalHandler.ts
    ├── Handler/BaseApprovalHandler.ts
    ├── Handler/HandymanApprover.ts
    ├── Handler/SupervisorApprover.ts
    ├── Handler/ManagerApprover.ts
    ├── Handler/DirectorApprover.ts
    ├── Service/JobApprovalService.ts
    └── App.ts

Previous: #12 — Proxy Pattern
Next up: #14 — Command Pattern
Encapsulating job actions; Assign, Pause, Cancel, Complete; as undoable command objects. Queue them, replay them, undo them.