Chain of Responsibility Pattern: Build Scalable, Extensible Validation Chains
The article explains the Chain of Responsibility design pattern, showing how to replace tangled nested if‑checks in order‑validation logic with a series of independent handler objects linked together, and demonstrates its implementation, variations, real‑world uses in filters, interceptors, and guidelines for when the pattern is appropriate.
When many validation steps are written as nested if statements inside a single method, the code becomes long, hard to maintain, and violates the Single‑Responsibility and Open‑Closed principles. The example below shows a typical order‑placement validation method:
public class OrderService {
public void placeOrder(OrderRequest req) {
// 1. inventory check
if (!inventory.enough(req)) {
throw new RuntimeException("库存不足");
}
// 2. purchase‑limit check
if (purchase.exceedLimit(req)) {
throw new RuntimeException("超过限购数量");
}
// 3. blacklist check
if (blacklist.contains(req.getUserId())) {
throw new RuntimeException("用户在黑名单");
}
// 4. risk‑control check
if (riskControl.hit(req)) {
throw new RuntimeException("触发风控");
}
// ... add more checks later
doPlaceOrder(req);
}
}Problems:
All checks share one method, making it hard to read.
Adding a new check requires modifying the method, breaking the Open‑Closed principle.
The order of checks is hard‑coded; changing the sequence needs code changes.
Individual checks cannot be reused elsewhere.
Chain of Responsibility solution
The pattern introduces an abstract handler that holds a reference to the next handler. Each concrete handler performs its own validation and either stops the chain (by throwing or returning) or forwards the request.
public abstract class OrderHandler {
protected OrderHandler next; // reference to the next handler
public OrderHandler setNext(OrderHandler next) {
this.next = next;
return next; // enable fluent chaining
}
public abstract void handle(OrderRequest req);
protected void handleNext(OrderRequest req) {
if (next != null) {
next.handle(req);
}
// when next is null the request has reached the end of the chain
}
}Concrete handlers implement the specific check:
public class InventoryHandler extends OrderHandler {
public void handle(OrderRequest req) {
if (!inventory.enough(req)) {
throw new RuntimeException("库存不足"); // stop the chain
}
handleNext(req); // pass to the next handler
}
}
public class LimitHandler extends OrderHandler {
public void handle(OrderRequest req) {
if (purchase.exceedLimit(req)) {
throw new RuntimeException("超过限购");
}
handleNext(req);
}
}
public class BlacklistHandler extends OrderHandler {
public void handle(OrderRequest req) {
if (blacklist.contains(req.getUserId())) {
throw new RuntimeException("黑名单");
}
handleNext(req);
}
}Assembling the chain and invoking it:
// Assemble: Inventory → Limit → Blacklist
OrderHandler chain = new InventoryHandler();
chain.setNext(new LimitHandler())
.setNext(new BlacklistHandler());
// The client sends the request to the head of the chain
chain.handle(orderRequest);Compared with the nested‑ if version, each validation is now an independent, reusable, and testable node. Adding a new check (e.g., real‑name verification) only requires a new handler class and inserting it into the chain; reordering is done by changing the assembly order. The pattern therefore satisfies both the Single‑Responsibility and Open‑Closed principles.
Roles and flow
Abstract handler – defines the handling interface and stores the next reference (e.g., OrderHandler).
Concrete handler – implements a single validation step and decides whether to forward the request (e.g., InventoryHandler, LimitHandler, BlacklistHandler).
Client – assembles the chain and invokes the head.
Each node decides either to call handleNext(req) (pass on) or to abort the chain by throwing an exception or returning a result (stop).
Semantic variants
All‑or‑nothing (veto) chain – every node must approve; a single failure aborts the whole process (common in risk‑control).
First‑handler‑wins – the request travels until the first capable handler processes it, then the chain stops (typical in approval workflows).
Real‑world appearances
Servlet Filter chain – each filter calls chain.doFilter() to continue.
Spring MVC HandlerInterceptor chain – interceptors run before the controller.
API‑gateway filter chains (Spring Cloud Gateway, Zuul) – sequential filters for rate limiting, authentication, routing, etc.
Netty ChannelPipeline – a high‑performance pipeline of ChannelHandler objects.
OkHttp Interceptor chain – HTTP request/response passes through a series of interceptors.
Any processing pipeline where a request passes through a configurable series of steps (often named Filter, Interceptor, Pipeline, or Chain) is an instance of the Chain of Responsibility.
Implementation forms
Linked‑list form – each node holds a next reference and invokes it directly (the example above). Simple but assembly can be verbose.
List‑based form – handlers are stored in a List and a separate executor iterates over them. Frameworks such as FilterChain use this style, making dynamic addition, removal, and configuration easier.
When to use
A request must pass through multiple processing steps that may be added, removed, or reordered.
Each step should be independent, reusable, and individually testable.
The sender does not need to know which concrete handler will finally process the request.
When not to use
If the processing consists of only two or three fixed steps, a simple sequential call is clearer.
If steps have complex mutual dependencies or require back‑and‑forth interaction, a single‑direction chain is unsuitable.
Practical tips
Ensure the chain has a definite termination point – either a handler that finishes processing or the end of the list.
Avoid excessively long or hidden chains; long chains make debugging difficult.
In summary, the Chain of Responsibility pattern replaces a monolithic if -heavy validation routine with a set of modular handlers linked together. It supports both veto and first‑handler semantics, can be organized as a linked list or a list‑based executor, and appears in servlet filters, gateway filters, Spring interceptors, Netty pipelines, and OkHttp interceptors. Proper use requires confirming that a request truly passes through a configurable series of independent steps; otherwise, the pattern may constitute over‑design.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
