Every order that enters PideYa must survive an obstacle course before reaching the kitchen: does it smell like fraud? Is every dish in stock? Do we deliver to that address? Does it reach the restaurant's minimum amount? Today that course lives in a single validation method that grows with every new check and that nobody can reorder, reuse, or test piece by piece. Chain of Responsibility proposes breaking it into independent links: each check is an object that decides whether to handle the request, reject it, or pass it along to the next one, and the chain is assembled — and reassembled — at runtime.
Contents
- The problem in PideYa: monolithic validation of the incoming order
- Intent and structure of the pattern
- Complete Java implementation
- Assembling the chain: configurable, not hard-wired
- Variants: stop on handle vs. process and continue
- The chain in real life: filters and middleware
- When to use it and when not to
- Relationship with other patterns
- Common mistakes
- Exercises and conclusion
The problem in PideYa: monolithic validation of the incoming order
The CheckoutFacade from module 3 starts its work by "validating" the order. This is what that validation looks like inside today:
public class OrderValidator {
public void validate(Order order) {
// 1. Fraud check
if (order.getCustomer().hasRecentDeclinedPayments()
&& order.getTotal().compareTo(new BigDecimal("100")) > 0) {
throw new OrderRejectedException("Possible fraud");
}
// 2. Stock for every dish
for (OrderLine line : order.getLines()) {
if (!stockService.inStock(line.getProduct())) {
throw new OrderRejectedException("Out of stock: " + line.getProduct());
}
}
// 3. Delivery zone
if (!zoneService.covers(order.getRestaurant(), order.getDeliveryAddress())) {
throw new OrderRejectedException("Outside the delivery zone");
}
// 4. Restaurant's minimum amount
if (order.getTotal().compareTo(order.getRestaurant().getMinimumAmount()) < 0) {
throw new OrderRejectedException("Below the minimum amount");
}
// ... and growing: opening hours, per-slot order limit...
}
}It works, but look at the forces pulling against each other:
- Every new check modifies the class (goodbye OCP); the method already mixes four responsibilities and their four dependencies (goodbye SRP).
- The order is hard-wired. The business wants to test whether checking the zone before the stock cuts down on expensive calls to the inventory service. Today that means rewriting the method.
- There is no reuse. The fraud check is also wanted when registering payment methods; the zone check, on the menu screen. Today: copy and paste.
- Context-specific configuration. Pickup orders don't need the zone check; employee orders need neither fraud nor minimum. Today: more
ifs wrapped around theifs. - Testing one check in isolation requires building the perfect order that dodges the three before it.
Stated precisely: there is a request (the incoming order) and several objects that could handle it, and we don't want the sender to know how many there are, who they are, or in what order they act.
Intent and structure of the pattern
Intent (GoF): 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.
Each handler knows only the next link. It receives the request and chooses: handle it (and decide whether the conversation ends) or delegate it. The sender only knows the first link.
classDiagram
class OrderHandler {
<<abstract>>
-next: OrderHandler
+chain(next: OrderHandler) OrderHandler
+handle(order: Order) ValidationResult
#check(order: Order)* ValidationResult
}
class FraudHandler {
#check(order: Order) ValidationResult
}
class StockHandler {
#check(order: Order) ValidationResult
}
class DeliveryZoneHandler {
#check(order: Order) ValidationResult
}
class MinimumAmountHandler {
#check(order: Order) ValidationResult
}
class CheckoutFacade {
-validationChain: OrderHandler
}
OrderHandler <|-- FraudHandler
OrderHandler <|-- StockHandler
OrderHandler <|-- DeliveryZoneHandler
OrderHandler <|-- MinimumAmountHandler
OrderHandler o-- OrderHandler : next
CheckoutFacade --> OrderHandler : uses the first one
| GoF role | In PideYa |
|---|---|
| Handler (interface/base class holding the reference to the next) | OrderHandler |
| ConcreteHandler | FraudHandler, StockHandler, DeliveryZoneHandler, MinimumAmountHandler |
| Client (sends the request to the first link) | CheckoutFacade |
The pattern's visual signature is that self-association next: a Handler that contains a Handler. Compare it with Decorator, which also chains — we'll come back to that classic confusion in the comparison.
The conversation over time, for an order that trips on the third check:
sequenceDiagram
participant C as CheckoutFacade
participant F as FraudHandler
participant S as StockHandler
participant Z as DeliveryZoneHandler
C->>F: handle(order)
F->>F: check: OK
F->>S: handle(order)
S->>S: check: OK
S->>Z: handle(order)
Z->>Z: check: outside zone
Z-->>C: rejected("Outside the delivery zone")
Note over Z: the 4th link never even hears about it
Complete Java implementation
The validation result, better as a value than as an exception (exceptions for normal business flow are expensive and noisy):
public record ValidationResult(boolean approved, String reason) {
public static ValidationResult ok() { return new ValidationResult(true, null); }
public static ValidationResult rejection(String reason) {
return new ValidationResult(false, reason);
}
}The base Handler concentrates the chain's plumbing, with the template trick that makes the concrete links trivial:
public abstract class OrderHandler {
private OrderHandler next;
/** Returns the link just added, enabling fluent chaining. */
public OrderHandler chain(OrderHandler next) {
this.next = next;
return next;
}
/** Shared plumbing: check this link and, if it approves, delegate. */
public ValidationResult handle(Order order) {
ValidationResult result = check(order);
if (!result.approved()) {
return result; // cuts the chain: final rejection
}
if (next == null) {
return ValidationResult.ok(); // end of the chain: everything approved
}
return next.handle(order); // delegate to the next link
}
/** The only thing each concrete link implements. */
protected abstract ValidationResult check(Order order);
}Two details worth explaining:
handleis public and final in practice: it defines the protocol (check → cut or delegate) exactly once. The links only fill incheck. (This "public method that fixes the flow + protected method that varies" is a miniature preview of Template Method.)- The cut is explicit: a rejection does not continue down the chain. That's the classic variant; we'll see the other one shortly.
The concrete links — each one small, with a single responsibility and its own injected dependencies (DIP):
public class FraudHandler extends OrderHandler {
private final FraudService fraudService;
public FraudHandler(FraudService fraudService) {
this.fraudService = fraudService;
}
@Override
protected ValidationResult check(Order order) {
return fraudService.isSuspicious(order)
? ValidationResult.rejection("Possible fraud")
: ValidationResult.ok();
}
}
public class MinimumAmountHandler extends OrderHandler {
@Override
protected ValidationResult check(Order order) {
BigDecimal minimum = order.getRestaurant().getMinimumAmount();
return order.getTotal().compareTo(minimum) < 0
? ValidationResult.rejection("Below the minimum amount of " + minimum + " €")
: ValidationResult.ok();
}
}(StockHandler and DeliveryZoneHandler follow the same mold with their respective services.)
Assembling the chain: configurable, not hard-wired
The point where this pattern pays its toll: someone has to assemble the chain. That someone is configuration code (the composition root, or a factory like those in module 2), never the links themselves:
OrderHandler chain = new FraudHandler(fraudService);
chain.chain(new DeliveryZoneHandler(zoneService)) // zone before stock:
.chain(new StockHandler(stockService)) // saves expensive calls
.chain(new MinimumAmountHandler());
CheckoutFacade checkout = new CheckoutFacade(chain, /* ... */);And here the forces the monolith couldn't serve come alive:
- Reordering means reordering lines of configuration.
- Chains per context: the pickup order assembles
Fraud → Stock → MinimumAmount(no zone); the internal employee order, justStock. Same links, different chains. - Testing a link means instantiating it on its own, chainless, and calling
check.
Variants: stop on handle vs. process and continue
The pure GoF variant is "exactly one handles it": the request advances until a handler takes it, and it dies there (think of support-ticket escalation: the bot resolves the frequently asked questions; what it doesn't know goes to the human agent; what the agent can't handle, to the supervisor — every ticket is resolved by one level). Our validation uses the complementary variant, just as common: "everyone weighs in until someone vetoes". And there is a third: "everyone always processes", where each link does its part and delegates unconditionally.
| Variant | Who processes | When it cuts | Example |
|---|---|---|---|
| First capable | Exactly one | At the first one that handles it | Support escalation: bot → agent → supervisor |
| All until veto | Everyone before the failure | At the first rejection | Our order validation |
| Full pipeline | Everyone | Never (except on error) | Filters that enrich the request in stages |
In all three, the essence is identical: sender decoupled from receivers, receivers decoupled from each other, chain assembled in configuration. Two honest warnings: in the pure GoF variant nobody guarantees the request gets handled — it can fall off the end of the chain, and you have to decide what happens then (default value, exception, entry in the EventLog) —; and in the veto variant, a link that forgets to delegate silently breaks every later check (which is why we concentrated the delegation in the base class).
The chain in real life: filters and middleware
If you've touched web development, you've already used this pattern without knowing it:
- Servlet filters (
jakarta.servlet.Filter): each filter receives the request and aFilterChain, does its part (authentication, compression, CORS...), and decides whether to callchain.doFilter(...)or cut. - The middleware of Express, ASP.NET Core, or Spring's interceptors: same idea, each piece processes and calls
next(). - The DOM's event bubbling: the click climbs the element hierarchy until something handles it.
The lesson from these examples is twofold: the pattern is very much alive (even though it's rarely called by its GoF name), and its natural habitat is configurable processing pipelines, more than the "exactly one responds" of the original book.
When to use it and when not to
Use it when:
- More than one object can handle a request and the sender shouldn't know which one.
- You want to add, remove, or reorder processing steps without touching the existing ones or the sender.
- The set of handlers must vary by context or configuration (even at runtime).
Avoid it when:
- There is a single, fixed receiver: a direct call is simpler and clearer. The one-link chain is textbook over-engineering (lesson 01-06).
- The sender needs a guarantee of handling and an immediate answer from a specific responsible party: the chain introduces uncertainty about who (and whether anyone) will respond.
- The ordering between steps hides strong data dependencies (each step needs the previous one's results): that's a pipeline with a contract between stages, and perhaps a clear sequential method is more honest than a rigid pseudo-chain.
The price to pay: the execution trace fragments (to know which checks apply to an order you have to look at the configuration, not one method), and debugging "why was it rejected?" means walking through links. A good ValidationResult with a reason, plus logging in the base class with EventLog, mitigate a lot.
Relationship with other patterns
Mentions only; each topic in its own lesson:
- Decorator: almost identical mechanics (linked objects that delegate), opposite intent — the decorator always delegates and adds; the handler decides whether to handle or pass. Full head-to-head in the comparison.
- Command: what travels along the chain can be a command; GoF often combines them (the reified request looking for its handler).
- Composite: in a tree, each node's parent is a natural "next" — requests can escalate toward the root.
- Facade: in PideYa, the checkout facade is the chain's client: it fires it without knowing its links.
Common mistakes
- Forgetting to delegate to the next link in a concrete handler (in variants where each concrete class manages the call): later checks vanish without a sound. Antidote: delegation plumbing in the base class, as we did.
- A chain with no defined end: in the "first capable" variant, not deciding what happens if nobody handles the request. Always define the default behavior (terminal handler, default value, or explicit exception).
- Links with mutable state shared between requests: the same chain usually serves concurrent requests; handlers should be stateless (like ours: only immutable dependencies).
- Putting the chain assembly inside the links (each one creating its next with
new): it resurrects the coupling we came to kill. Assembly belongs to configuration. - Mile-long chains for trivial logic: four consecutive
ifs that will never change don't need four classes. The pattern earns its keep when there is real variability (order, context, reuse).
Exercises
Exercise 1: a new link without touching anything
The business asks for a new check: reject orders if the restaurant is closed during the requested time slot (order.getDeliverySlot(), restaurant.openAt(slot)). Write OpeningHoursHandler and say where you would place it in the chain and why.
Exercise 2: support escalation chain
Model the ticket escalation with the "first capable" variant: SupportBot resolves the ticket if it is of type FREQUENTLY_ASKED; SupportAgent resolves it if affectedAmount <= 50; SupportSupervisor resolves everything else. Design the base class TicketHandler (how does its plumbing differ from OrderHandler?) and the SupportBot link.
Exercise 3: spot the anti-pattern
A colleague implements StockHandler like this: if (!inStock) return rejection(...); else return new DeliveryZoneHandler(zones).handle(order);. Point out the two design problems.
Solutions
Solution 1:
public class OpeningHoursHandler extends OrderHandler {
@Override
protected ValidationResult check(Order order) {
return order.getRestaurant().openAt(order.getDeliverySlot())
? ValidationResult.ok()
: ValidationResult.rejection("Restaurant closed during that slot");
}
}A reasonable placement: at the start (after fraud, or even before it): it's a local, dirt-cheap check that can save expensive calls to stock and zones. The important part of the exercise: adding it touches nothing — not the base, not the other links, not the facade — just one line of configuration. OCP in action.
Solution 2: the plumbing changes in the cut condition — the chain cuts when someone handles, not when someone rejects; and if nobody handles it, you must decide the ending (here, the supervisor is terminal, but the base class should protect itself anyway):
public abstract class TicketHandler {
private TicketHandler next;
public TicketHandler chain(TicketHandler n) {
this.next = n;
return n;
}
public Resolution handle(Ticket ticket) {
if (canResolve(ticket)) {
return resolve(ticket); // first capable: it ends here
}
if (next == null) {
throw new IllegalStateException("Ticket with no owner: " + ticket.getId());
}
return next.handle(ticket); // escalate
}
protected abstract boolean canResolve(Ticket ticket);
protected abstract Resolution resolve(Ticket ticket);
}
public class SupportBot extends TicketHandler {
@Override protected boolean canResolve(Ticket t) {
return t.getType() == TicketType.FREQUENTLY_ASKED;
}
@Override protected Resolution resolve(Ticket t) {
return Resolution.automatic(answers.lookup(t));
}
}Solution 3: (1) the link creates its next with new, hard-wiring the order inside the handler — the chain stops being configurable and StockHandler couples itself to DeliveryZoneHandler and its dependencies; (2) it duplicates the delegation plumbing that already lives in the base class, so any protocol change (logging, metrics) will have to be replicated link by link. Delegation belongs to the base; assembly belongs to configuration.
Conclusion
Chain of Responsibility turns a monolithic sequence of checks into autonomous links: each OrderHandler performs a single check, ignores its neighbors, and the complete chain is decided in configuration — reorderable, trimmable per context, and testable piece by piece. You've seen its three variants (first capable, veto, pipeline), its ubiquitous presence in filters and middleware, and its price: the fragmented trace and the responsibility of defining what happens at the end of the chain.
What traveled along the chain was the order; the next pattern reifies the journey itself. In PideYa's restaurant panel, "accept order", "mark as preparing", or "cancel" are today calls that execute and evaporate: they can't be queued, or logged, or — what the restaurant asks for most — undone. For that, each request must become an object with a life of its own. See you in Command.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
