Design Patterns

Chain of Responsibility Design Pattern

If you've used Express middleware, you already know this pattern. A request comes in. The first middleware checks auth. If it passes, it calls next(). The...

23 Mar 2024

Chain of Responsibility Design Pattern

If you've used Express middleware, you already know this pattern. A request comes in. The first middleware checks auth. If it passes, it calls next(). The second middleware logs the request. The third parses the body. Each handler decides: do I handle this, or pass it along?

That's Chain of Responsibility. You build a chain of handler objects. A request enters the chain. Each handler either processes it or forwards it to the next handler.

Think of it like a corporate approval process. A junior manager can approve expenses under $500. Over that, it goes to the senior manager. Over $5,000, it goes to the VP. Each level checks if they can handle it and either approves or escalates.

Javascript
class SupportAgent {
  constructor(name, nextAgent = null) {
    this.name = name;
    this.nextAgent = nextAgent;
  }

  handleRequest(ticket) {
    if (this.canHandle(ticket)) {
      console.log(`${this.name} is handling ticket: ${ticket.description}`);
    } else if (this.nextAgent) {
      console.log(`${this.name} cannot handle ticket, passing to ${this.nextAgent.name}`);
      this.nextAgent.handleRequest(ticket);
    } else {
      console.log(`No agent available to handle ticket: ${ticket.description}`);
    }
  }

  canHandle(ticket) {
    return false;
  }
}

class LevelOneSupport extends SupportAgent {
  canHandle(ticket) {
    return ticket.level === 1;
  }
}

class LevelTwoSupport extends SupportAgent {
  canHandle(ticket) {
    return ticket.level === 2;
  }
}

class LevelThreeSupport extends SupportAgent {
  canHandle(ticket) {
    return ticket.level === 3;
  }
}

const agent1 = new LevelOneSupport("Alice");
const agent2 = new LevelTwoSupport("Bob", agent1);
const agent3 = new LevelThreeSupport("Charlie", agent2);

agent3.handleRequest({ level: 2, description: "Internet connectivity issue" });
// Charlie cannot handle ticket, passing to Bob
// Bob is handling ticket: Internet connectivity issue

agent3.handleRequest({ level: 3, description: "Server outage" });
// Charlie is handling ticket: Server outage

agent3.handleRequest({ level: 1, description: "Printer malfunction" });
// Charlie cannot handle ticket, passing to Bob
// Bob cannot handle ticket, passing to Alice
// Alice is handling ticket: Printer malfunction

Each SupportAgent checks canHandle(). If it can, it processes the ticket. If not, it passes it to nextAgent. The chain ends when either a handler processes the request or there's no next handler.

The sender doesn't know (or care) which handler ultimately processes the request. That's the decoupling.

The benefit: You can add, remove, or reorder handlers without touching the sender. Each handler has a single responsibility. The chain is dynamic — you can assemble it at runtime.

The cost: No guarantee any handler will process the request. The request might fall off the end of the chain silently. Debugging can be tricky because you need to trace through the entire chain to see what happened. Long chains also add latency.

I use this pattern for middleware pipelines, validation chains, event handling systems, and approval workflows. If you find yourself writing long if/else if/else if blocks where each branch checks a different condition, consider replacing it with a chain.

Keep reading