# Chain of Responsibility

> Pass a request along a chain of handlers until one decides it, so the sender never knows which handler acts or how many exist.

- kind: pattern
- category: behavioral
- aliases: CoR, Chain of Command
- canonical: https://aipatterns.dev/design-patterns/chain-of-responsibility
- inspired-by: https://refactoring.guru/design-patterns/chain-of-responsibility

## Intent

Give several objects a chance to handle a request by linking them into a chain and passing the request along it. Each handler either decides the request or forwards it to the next one. The sender talks to the head of the chain and never learns who answered.

## Forces

- **One entry point for the sender** vs **many rules that each own part of the decision**: callers want one call, but the logic belongs to separate checks.
- **Rule order matters** vs **rules should not know each other**: a duplicate check must run before auto-approval, yet neither should reference the other.
- **Different chains per context** vs **one hard-coded `if/else` ladder**: retail and marketplace orders need different rule sets, assembled at startup.
- **Work around the decision** vs **work inside it**: logging, timing or auditing must wrap whatever the later handlers decide.

## Therefore

**Give every handler the same interface and a reference to the next handler, and let each one either return a result or forward the request.**

The client builds the chain once, usually at the composition root, and calls the first handler. A handler that cannot decide calls `next`. A handler can also run code before and after forwarding, which is what makes the linked form different from a plain list of rules. When no handler decides, the end of the chain returns a defined fallback instead of failing silently.

## Structure

```mermaid
classDiagram
  class RefundHandler {
    <<interface>>
    +handle(req) Decision
  }
  class Step {
    <<abstract>>
    -RefundHandler next
    +handle(req) Decision
  }
  class AuditTrail {
    +handle(req) Decision
  }
  class DuplicateGuard {
    +handle(req) Decision
  }
  class FraudHold {
    +handle(req) Decision
  }
  class AutoApprove {
    -Money limit
    +handle(req) Decision
  }
  RefundHandler <|.. Step
  Step o-- RefundHandler : next
  Step <|-- AuditTrail
  Step <|-- DuplicateGuard
  Step <|-- FraudHold
  Step <|-- AutoApprove
```

### Participants

- **Handler** (`RefundHandler`): the one method every link implements. It takes the request and returns the outcome.
- **Base handler** (`Step`): optional. It stores `next` and supplies the forwarding and end-of-chain fallback, so concrete handlers don't repeat it.
- **Concrete handlers** (`DuplicateGuard`, `FraudHold`, `AutoApprove`, `AuditTrail`): each checks one condition, decides or forwards, and knows nothing about its neighbors.
- **Client**: assembles the chain in the right order and sends requests to its head.

## Use it when

- A request goes through a sequence of checks where any one of them may settle it: refund triage, support ticket routing, input validation with early rejection.
- The set or order of checks differs by tenant, plan, region or environment, and you want to assemble it from configuration.
- Some handlers must wrap the rest of the chain: audit logging, timing, retries, or transactions around whatever decides.
- A long `if/else` ladder in one function keeps growing, and each branch is an independent business rule with its own tests.

## Do not use it when

- Every handler always runs in order and none can stop the flow. That is a pipeline: use an array of functions and a loop, or your framework's middleware stack (Express, Koa, ASP.NET, Go `http.Handler` wrapping). Most web frameworks already give you this pattern; don't rebuild it.
- The rules are pure predicates that map to an outcome and nothing wraps the call. A list of `(condition, decision)` pairs and a `find` is shorter and easier to read than linked objects.
- There are two or three fixed checks that never change order or vary by context. Keep the `if` statements.
- Exactly one receiver is known at compile time. Call it directly.
- Several receivers must *all* react to the event. That is [Observer](https://aipatterns.dev/design-patterns/observer.md), not a chain.

## Application grill

**Inspect first:** the framework middleware already in place (Express, Koa, Next, Fastify middleware, interceptors, guards) - the codebase may already run this pattern under another name; how handlers decide today to act or pass along.

**Missing pieces:** usually none: the framework's middleware chain is the pattern, and a plain array of functions covers short chains. A pipeline library pays only when users configure chains at runtime.

**Ask before you build:**

- **Scale:** how many handlers will the chain hold - three steps is an array, fifteen is a framework decision?
- **Performance:** does every request traverse every handler even when the first would decide?
- **Reliability:** when no handler claims the request, what answers - a terminal default handler or an error?
- **Budget:** handler order is invisible at call sites; who owns keeping it correct?
- **Implementation:** is the chain order fixed in code or configurable, and is there a test per decision boundary?

## Implementation steps

1. List the rules in the current conditional and confirm each one can either decide or defer on its own.
2. Define the handler interface: one method that takes the request and returns a decision. Keep the request immutable so handlers can't change it behind each other's backs.
3. Add a base handler (or a helper function) that holds `next` and returns a defined fallback when the chain ends.
4. Move each rule into its own handler. Run the tests after each move.
5. Build the chain at the composition root, in one place, so the order is visible and reviewable.
6. Replace the original conditional with one call to the head of the chain.

## Example

A store triages refund requests. Duplicates are rejected, flagged customers are held for review, small refunds are approved automatically, and everything else goes to a manager. Marketplace orders use a stricter chain with no auto-approval. Every decision is audited.

```ts title="refunds.ts"
interface RefundRequest {
  orderId: string;
  amountCents: number;
  customerFlagged?: boolean;
  alreadyRefunded?: boolean;
}

type Decision = { outcome: "approved" | "rejected" | "held" | "escalated"; reason: string };

interface RefundHandler {
  handle(req: RefundRequest): Decision;
}

abstract class Step implements RefundHandler {
  constructor(private readonly next?: RefundHandler) {}

  abstract handle(req: RefundRequest): Decision;

  protected passOn(req: RefundRequest): Decision {
    if (!this.next) return { outcome: "escalated", reason: "no rule matched; send to a manager" };
    return this.next.handle(req);
  }
}

class DuplicateGuard extends Step {
  handle(req: RefundRequest): Decision {
    return req.alreadyRefunded ? { outcome: "rejected", reason: "order already refunded" } : this.passOn(req);
  }
}

class FraudHold extends Step {
  handle(req: RefundRequest): Decision {
    return req.customerFlagged ? { outcome: "held", reason: "customer under review" } : this.passOn(req);
  }
}

class AutoApprove extends Step {
  constructor(private readonly limitCents: number, next?: RefundHandler) {
    super(next);
  }
  handle(req: RefundRequest): Decision {
    return req.amountCents <= this.limitCents ? { outcome: "approved", reason: "under auto limit" } : this.passOn(req);
  }
}

// Wraps the rest of the chain: it forwards first, then records the result.
class AuditTrail extends Step {
  handle(req: RefundRequest): Decision {
    const decision = this.passOn(req);
    console.log(`[audit] ${req.orderId} -> ${decision.outcome}: ${decision.reason}`);
    return decision;
  }
}

// Order is set here, once. Each handler only knows "next".
const retail = new AuditTrail(new DuplicateGuard(new FraudHold(new AutoApprove(5000))));
const marketplace = new AuditTrail(new DuplicateGuard(new FraudHold()));

retail.handle({ orderId: "A-100", amountCents: 2500 }); // approved
retail.handle({ orderId: "A-101", amountCents: 2500, customerFlagged: true }); // held
retail.handle({ orderId: "A-102", amountCents: 9900 }); // escalated
retail.handle({ orderId: "A-103", amountCents: 900, alreadyRefunded: true }); // rejected
marketplace.handle({ orderId: "M-200", amountCents: 2500 }); // escalated
```

```python title="refunds.py"
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class RefundRequest:
    order_id: str
    amount_cents: int
    customer_flagged: bool = False
    already_refunded: bool = False

@dataclass(frozen=True)
class Decision:
    outcome: str  # approved | rejected | held | escalated
    reason: str

class Step:
    def __init__(self, next_step: Optional[Step] = None) -> None:
        self._next = next_step

    def handle(self, req: RefundRequest) -> Decision:
        return self.pass_on(req)

    def pass_on(self, req: RefundRequest) -> Decision:
        if self._next is None:
            return Decision("escalated", "no rule matched; send to a manager")
        return self._next.handle(req)

class DuplicateGuard(Step):
    def handle(self, req: RefundRequest) -> Decision:
        if req.already_refunded:
            return Decision("rejected", "order already refunded")
        return self.pass_on(req)

class FraudHold(Step):
    def handle(self, req: RefundRequest) -> Decision:
        if req.customer_flagged:
            return Decision("held", "customer under review")
        return self.pass_on(req)

class AutoApprove(Step):
    def __init__(self, limit_cents: int, next_step: Optional[Step] = None) -> None:
        super().__init__(next_step)
        self.limit_cents = limit_cents

    def handle(self, req: RefundRequest) -> Decision:
        if req.amount_cents <= self.limit_cents:
            return Decision("approved", "under auto limit")
        return self.pass_on(req)

class AuditTrail(Step):
    def handle(self, req: RefundRequest) -> Decision:
        decision = self.pass_on(req)  # forward first, then record
        print(f"[audit] {req.order_id} -> {decision.outcome}: {decision.reason}")
        return decision

if __name__ == "__main__":
    retail = AuditTrail(DuplicateGuard(FraudHold(AutoApprove(5_000))))
    marketplace = AuditTrail(DuplicateGuard(FraudHold()))  # sellers never auto-approve

    retail.handle(RefundRequest("A-100", 2_500))  # approved
    retail.handle(RefundRequest("A-101", 2_500, customer_flagged=True))  # held
    retail.handle(RefundRequest("A-102", 9_900))  # escalated
    retail.handle(RefundRequest("A-103", 900, already_refunded=True))  # rejected
    marketplace.handle(RefundRequest("M-200", 2_500))  # escalated
```

```go title="refunds.go"
package main

import "fmt"

type RefundRequest struct {
	OrderID         string
	AmountCents     int
	CustomerFlagged bool
	AlreadyRefunded bool
}

type Decision struct{ Outcome, Reason string }

type Handler interface {
	Handle(r RefundRequest) Decision
}

// passOn forwards to next, or escalates when the chain runs out.
func passOn(next Handler, r RefundRequest) Decision {
	if next == nil {
		return Decision{"escalated", "no rule matched; send to a manager"}
	}
	return next.Handle(r)
}

type DuplicateGuard struct{ Next Handler }

func (h DuplicateGuard) Handle(r RefundRequest) Decision {
	if r.AlreadyRefunded {
		return Decision{"rejected", "order already refunded"}
	}
	return passOn(h.Next, r)
}

type FraudHold struct{ Next Handler }

func (h FraudHold) Handle(r RefundRequest) Decision {
	if r.CustomerFlagged {
		return Decision{"held", "customer under review"}
	}
	return passOn(h.Next, r)
}

type AutoApprove struct {
	LimitCents int
	Next       Handler
}

func (h AutoApprove) Handle(r RefundRequest) Decision {
	if r.AmountCents <= h.LimitCents {
		return Decision{"approved", "under auto limit"}
	}
	return passOn(h.Next, r)
}

// AuditTrail wraps the rest of the chain and records its result.
type AuditTrail struct{ Next Handler }

func (h AuditTrail) Handle(r RefundRequest) Decision {
	d := passOn(h.Next, r)
	fmt.Printf("[audit] %s -> %s: %s\n", r.OrderID, d.Outcome, d.Reason)
	return d
}

func main() {
	retail := AuditTrail{Next: DuplicateGuard{Next: FraudHold{Next: AutoApprove{LimitCents: 5000}}}}
	marketplace := AuditTrail{Next: DuplicateGuard{Next: FraudHold{}}}

	retail.Handle(RefundRequest{OrderID: "A-100", AmountCents: 2500})                        // approved
	retail.Handle(RefundRequest{OrderID: "A-101", AmountCents: 2500, CustomerFlagged: true}) // held
	retail.Handle(RefundRequest{OrderID: "A-102", AmountCents: 9900})                        // escalated
	retail.Handle(RefundRequest{OrderID: "A-103", AmountCents: 900, AlreadyRefunded: true})  // rejected
	marketplace.Handle(RefundRequest{OrderID: "M-200", AmountCents: 2500})                   // escalated
}
```

```java title="RefundTriage.java"
public class RefundTriage {
    record RefundRequest(String orderId, long amountCents, boolean customerFlagged, boolean alreadyRefunded) {}

    record Decision(String outcome, String reason) {}

    interface RefundHandler {
        Decision handle(RefundRequest req);
    }

    abstract static class Step implements RefundHandler {
        private final RefundHandler next;

        Step(RefundHandler next) { this.next = next; }

        protected Decision passOn(RefundRequest req) {
            if (next == null) return new Decision("escalated", "no rule matched; send to a manager");
            return next.handle(req);
        }
    }

    static final class DuplicateGuard extends Step {
        DuplicateGuard(RefundHandler next) { super(next); }

        public Decision handle(RefundRequest req) {
            return req.alreadyRefunded() ? new Decision("rejected", "order already refunded") : passOn(req);
        }
    }

    static final class FraudHold extends Step {
        FraudHold(RefundHandler next) { super(next); }

        public Decision handle(RefundRequest req) {
            return req.customerFlagged() ? new Decision("held", "customer under review") : passOn(req);
        }
    }

    static final class AutoApprove extends Step {
        private final long limitCents;

        AutoApprove(long limitCents, RefundHandler next) { super(next); this.limitCents = limitCents; }

        public Decision handle(RefundRequest req) {
            return req.amountCents() <= limitCents ? new Decision("approved", "under auto limit") : passOn(req);
        }
    }

    static final class AuditTrail extends Step {
        AuditTrail(RefundHandler next) { super(next); }

        public Decision handle(RefundRequest req) {
            Decision d = passOn(req); // forward first, then record
            System.out.printf("[audit] %s -> %s: %s%n", req.orderId(), d.outcome(), d.reason());
            return d;
        }
    }

    public static void main(String[] args) {
        RefundHandler retail = new AuditTrail(new DuplicateGuard(new FraudHold(new AutoApprove(5000, null))));
        RefundHandler marketplace = new AuditTrail(new DuplicateGuard(new FraudHold(null)));

        retail.handle(new RefundRequest("A-100", 2500, false, false)); // approved
        retail.handle(new RefundRequest("A-101", 2500, true, false));  // held
        retail.handle(new RefundRequest("A-102", 9900, false, false)); // escalated
        retail.handle(new RefundRequest("A-103", 900, false, true));   // rejected
        marketplace.handle(new RefundRequest("M-200", 2500, false, false)); // escalated
    }
}
```

## Trade-offs

**Gains**
- The sender is decoupled from the receivers. Adding, removing or reordering a rule touches only the chain assembly.
- Each rule is a small class or function you can test with a stub `next`.
- Different contexts get different chains from the same parts.
- Handlers can wrap the rest of the chain for auditing, timing or error handling.

**Costs**
- A request can fall off the end unhandled. You need an explicit fallback, and tests for it.
- Order dependencies are real but implicit: moving `AutoApprove` above `FraudHold` is a silent bug.
- Debugging means stepping through every link to find who decided.
- For pure predicate rules, it is more code than a list and a loop.

## Relations

- [Decorator](https://aipatterns.dev/design-patterns/decorator.md) has the same linked shape, but every decorator always forwards and adds behavior. A chain handler may stop the request.
- [Command](https://aipatterns.dev/design-patterns/command.md) can be the request object that travels down the chain, which makes requests loggable and replayable.
- [Composite](https://aipatterns.dev/design-patterns/composite.md) trees often use a chain upward: a UI element that can't handle a click passes it to its parent.
- [Mediator](https://aipatterns.dev/design-patterns/mediator.md) centralizes the routing decision in one object. A chain distributes it across handlers.
- [Observer](https://aipatterns.dev/design-patterns/observer.md) delivers an event to every subscriber. A chain delivers it until one handler takes it.

## Agent checklist

- [ ] At least one handler can stop the request or wraps the rest of the chain. Otherwise use a function array or the framework's middleware.
- [ ] The chain is assembled in one visible place, and the order is covered by a test.
- [ ] The end of the chain returns a defined fallback (escalate, reject, or a typed "unhandled"), never `undefined` by accident.
- [ ] The request object is immutable, or the handlers that may change it are documented.
- [ ] Each handler has its own test with a stub `next`, and the original conditional's behavior is preserved, verified by running the existing tests.
