Chain of Responsibility - Pattern Clarity #9

Chain of Responsibility - Pattern Clarity #9

The Chain of Responsibility design pattern allows you to pass a request through a series of handlers (objects), where each one can decide whether to handle it or forward it to the next. This approach is particularly helpful when multiple objects might handle a request, but you don't want the request sender tightly coupled to a specific receiver.


❗ Problem

Suppose you're building a web application where each incoming request must pass a series of checks: authentication, authorization, rate-limiting, validation, and logging. Initially, you might code these in a single block:

if (Authenticate(request)) {
    if (Authorize(request)) {
        if (RateLimitCheck(request)) {
            if (Validate(request)) {
                LogRequest(request);
                // ... proceed ...
            }
        }
    }
}        

This approach soon reveals flaws:

  • Rigid structure: adding or removing a check means editing this method, risking bugs.
  • Lack of separation: each check is just lumped together, reducing clarity.
  • Difficulty reusing checks: if you want to apply some subset of checks elsewhere, you end up duplicating code.
  • Hard to debug: figuring out which check failed requires stepping through multiple nested conditionals.


✅ Solution

The Chain of Responsibility pattern provides a neater way to handle sequential checks by linking handler objects in a chain. Each handler knows only about its successor, and when it can't handle (or partially handles) the request, it passes it on.

Article content

Key elements include:

  • Handler abstraction: defines a method (e.g., handle(request)) and a reference to the next handler.
  • Concrete handlers: implement the logic for a specific check (handlerA, handlerB, handlerC). If they can't handle the request fully, they forward it.
  • Client: sends the request into the chain, unaware of who ultimately processes it.

Article content
Chain of Responsibility class diagram.

This design spreads out the checks into separate, small classes that form a pipeline. It becomes simple to insert a new handler or reorder them, without modifying existing code.


📚 Use Cases

  • Middleware pipelines: common in web frameworks (e.g., ASP.NET Core, Node.js Express), where each middleware does its task, then calls the next.
  • Approval workflows: a purchase request goes from a Team Lead → Manager → Director, each deciding whether to approve or escalate.
  • GUI event handling: an event "bubbles" from a child element up through parent containers until handled.
  • Logging chains: multiple loggers (console, file, remote) can each process a log message in sequence.


⚖️ Benefits & Drawbacks

Pros:

  • Better organization: each check or handler is encapsulated in its own class.
  • Flexibility: adding new handlers is easy - just insert them into the chain.
  • Decoupling: the client has no idea which handler is ultimately responsible.

Cons:

  • Debug complexity: a request might pass through many handlers, so diagnosing issues can be harder.
  • Order sensitivity: handlers might depend on a certain sequence (e.g., authenticate before authorize).
  • Performance: a long chain means multiple calls and checks for each request, which might become costly at scale.

❗️ Distributed or multiple processes: the chain isn't automatically shared across machines. You might need a message queue or pub-sub approach if you want to distribute handling steps among different services.


🔧 How to implement

  1. Create an abstract handler: define a base class or interface with a Handle(request) method and a reference to the next handler.
  2. Implement concrete handlers: each one decides if it handles or forwards the request.
  3. Build the chain: in your setup code, link the handlers in the order you want.
  4. Refactor your calling code: replace direct checks with a single call to the first handler in the chain.
  5. Provide a "terminal" handler to catch any unhandled requests at the end (optional).

By using this pattern, you can break large "if-else" blocks into a pipeline of smaller classes, each with a single responsibility.


🔄 Handling concurrency

When multiple threads use the same chain:

  1. Avoid mutable shared state in handlers (unless properly synchronized).
  2. Asynchronous handlers: if each handler is async, the request can be "awaited" before passing to the next. Great for I/O-heavy operations (e.g., database calls).
  3. Scaled environment: if your system is distributed, you might use a queue or service bus to emulate a chain across services, and you must handle reliability, timeouts, etc.


💻 Code Example

Below is a simplified C# example that processes expense requests via a chain of approvers:

namespace PatternClarity.BehavioralPatterns.ChainOfResponsibility;

class PurchaseRequest
{
    public int Id { get; }
    public double Amount { get; }
    public string Description { get; }

    public PurchaseRequest(int id, double amount, string description)
    {
        Id = id;
        Amount = amount;
        Description = description;
    }
}

abstract class Approver
{
    protected Approver next;

    public Approver SetNext(Approver approver)
    {
        next = approver;
        return approver;
    }

    public abstract void ProcessRequest(PurchaseRequest request);
}

class TeamLead : Approver
{
    public override void ProcessRequest(PurchaseRequest request)
    {
        if (request.Amount < 1000)
            Console.WriteLine($"TeamLead approved request #{request.Id}");
        else if (next != null)
            next.ProcessRequest(request);
    }
}

class Manager : Approver
{
    public override void ProcessRequest(PurchaseRequest request)
    {
        if (request.Amount < 10000)
            Console.WriteLine($"Manager approved request #{request.Id}");
        else if (next != null)
            next.ProcessRequest(request);
    }
}

class Director : Approver
{
    public override void ProcessRequest(PurchaseRequest request)
    {
        if (request.Amount < 50000)
            Console.WriteLine($"Director approved request #{request.Id}");
        else
            Console.WriteLine($"Request #{request.Id} requires special board approval!");
    }
}

class Program
{
    static void Main()
    {
        // Build the chain
        Approver teamLead = new TeamLead();
        Approver manager = new Manager();
        Approver director = new Director();
        teamLead.SetNext(manager).SetNext(director);

        // Test requests
        teamLead.ProcessRequest(new PurchaseRequest(1, 500, "Office Supplies"));
        teamLead.ProcessRequest(new PurchaseRequest(2, 5000, "Workstations"));
        teamLead.ProcessRequest(new PurchaseRequest(3, 30000, "New Servers"));
        teamLead.ProcessRequest(new PurchaseRequest(4, 120000, "Data Center Upgrade"));
    }
}        

Console output:

TeamLead approved request #1
Manager approved request #2
Director approved request #3
Request #4 requires special board approval!
        

Here, each approver either handles the request or passes it on. You can easily insert new steps (e.g., VicePresident) by adjusting the chain construction.


Conclusion

Chain of Responsibility is a powerful way to organize multiple potential handlers for a request, preventing massive if-else blocks. By distributing tasks among small, testable handler classes, you enhance maintainability. But beware of too many handlers, tricky ordering, and potential debugging hurdles.

Sometimes you might use a framework's built-in middleware pipeline, or even a pub-sub approach for looser coupling in distributed systems. Choose what fits your needs best. When done right, CoR keeps your code flexible and clean!

Thank you for reading this article in the "Pattern Clarity" series! I'd love to hear your thoughts and experiences with the Chain of Responsibility pattern - share your comments and suggestions. Let's keep the conversation going!

To view or add a comment, sign in

More articles by Ivan Vydrin

  • Composite - Pattern Clarity #17

    Composite is a structural design pattern that composes objects into tree structures, allowing a group of objects to be…

  • Your Essential Guide to Health Checks in .NET

    Health checks are automated diagnostics used to determine whether an application and its dependencies are functioning…

    9 Comments
  • Claude Code: Anthropic's AI Coding Assistant

    Claude Code is Anthropic's innovative command-line AI assistant designed to streamline software development. Whether…

    5 Comments
  • Flyweight - Pattern Clarity #16

    The Flyweight pattern is a structural design approach that optimizes memory usage by allowing multiple objects to share…

    2 Comments
  • Agile Planning Poker ♣️ - more than a Game for Estimates

    We've all been there: a sprint planning meeting where the team debates how many story points a "simple" feature really…

    4 Comments
  • Get started with the official Microsoft's Azure MCP Server

    Microsoft's Azure MCP Server - where MCP stands for Model Context Protocol - is a new tool in public preview that…

    3 Comments
  • Visitor - Pattern Clarity #15

    Visitor is a behavioral design pattern that lets you define new operations on objects without changing their classes…

    2 Comments
  • Agentic AI implementation guidance

    Building on our exploration of Agentic AI and the transformative potential of autonomous agents, this article takes a…

    1 Comment
  • Memento - Pattern Clarity #14

    The Memento design pattern lets an object capture its internal state and save it externally so that it can be restored…

  • Microsoft Copilot Studio: a Low‑Code AI Assistant Builder

    Imagine having your own AI "copilot" that can answer questions, perform tasks, and streamline work for you - all…

    4 Comments

Insights from the community

Others also viewed

Explore topics