Why I Won’t Take the Blame: Mastering the Chain of Responsibility Pattern
This article explains the Chain of Responsibility design pattern, shows classic Java implementations, demonstrates its use in Spring Security, provides real‑world examples such as order processing, logging pipelines, and approval workflows, and offers advanced functional and asynchronous techniques along with performance tips and common pitfalls.
Introduction
In the workplace, "passing the buck" is an art. In programming, the same idea becomes the elegant Chain of Responsibility pattern , where a request travels along a chain and each handler can either process it or pass it to the next.
Core Concepts
The pattern consists of three main roles:
Handler (abstract handler) : defines the handling interface and holds a reference to the next handler.
ConcreteHandler : implements the actual processing logic and decides whether to return a result or delegate to the next handler.
Client : assembles the chain and initiates the request.
The essence is to decouple the request sender from the receiver, allowing multiple objects a chance to handle the request.
Classic Implementations
Traditional (abstract class + inheritance)
public abstract class Handler {
protected Handler nextHandler;
public Handler setNext(Handler nextHandler) { this.nextHandler = nextHandler; return nextHandler; }
public final Object handle(Request request) {
Object result = doHandle(request);
if (result != null) return result;
if (nextHandler != null) return nextHandler.handle(request);
return handleDefault(request);
}
protected abstract Object doHandle(Request request);
protected Object handleDefault(Request request) { return new Result(false, "Request cannot be handled"); }
}
public class HandlerA extends Handler { /* implementation */ }
public class HandlerB extends Handler { /* implementation */ }
public class HandlerC extends Handler { /* implementation */ }
public class Client {
public static void main(String[] args) {
Handler handlerA = new HandlerA();
Handler handlerB = new HandlerB();
Handler handlerC = new HandlerC();
handlerA.setNext(handlerB).setNext(handlerC);
// create requests of type A, B, C, D and invoke handlerA.handle(request)
}
}Interface‑based (more flexible)
public interface RequestHandler {
Result handle(Request request, RequestChain chain);
}
public class RequestChain {
private final List<RequestHandler> handlers;
private int currentIndex = 0;
public RequestChain(List<RequestHandler> handlers) { this.handlers = handlers; }
public Result proceed(Request request) { /* invoke next handler or return default */ }
public Result execute(Request request) { currentIndex = 0; return proceed(request); }
}Functional (lambda style)
@FunctionalInterface
public interface FunctionHandler<T, R> {
R apply(T input, Function<T, R> next);
default FunctionHandler<T, R> andThen(FunctionHandler<T, R> after) {
return (input, next) -> this.apply(input, t -> after.apply(t, next));
}
}Chain of Responsibility in Spring Security
Spring Security implements the pattern through a Security Filter Chain . The request passes through a series of filters such as ChannelProcessingFilter, SecurityContextPersistenceFilter, UsernamePasswordAuthenticationFilter, and finally reaches the controller. Custom filters (e.g., a JWT authentication filter) can be inserted before or after any built‑in filter.
Real‑World Scenarios
Scenario 1 – E‑commerce Order Processing Pipeline
Steps: stock check → coupon validation → payment pre‑processing → risk control → order creation. Each step is a OrderHandler implementation (e.g., StockCheckHandler, CouponValidationHandler, PaymentPreHandler, RiskControlHandler, OrderCreateHandler). The OrderService wires all handlers via Spring’s @Order annotation and executes the chain.
Scenario 2 – Logging Pipeline
Handlers include LevelFilterHandler (drops low‑level logs), SensitiveDataFilterHandler (redacts passwords, tokens), FormatHandler (adds timestamp and level), AsyncAppenderHandler (writes to console, file, DB asynchronously), and AlertHandler (alerts on ERROR/FATAL). The Logger class builds a LogChain and exposes debug/info/warn/error methods.
Scenario 3 – Multi‑Level Approval Workflow
Handlers such as DirectManagerHandler, HRApprovalHandler, FinanceApprovalHandler, and DirectorApprovalHandler form a dynamic ApprovalChain. The chain is selected based on request type (LEAVE, EXPENSE, PURCHASE) and can be cached per type.
Advanced Usage
Functional Chain with Java Streams
ChainBuilder<String, String> builder = new ChainBuilder<>();
String result = builder
.addStageIf(s -> s.length() > 10, s -> { System.out.println("[1] Truncate"); return s.substring(0,10); })
.addStage(s -> { System.out.println("[2] Uppercase"); return s.toUpperCase(); })
.addStageIf(s -> !s.endsWith("!"), s -> { System.out.println("[3] Add exclamation"); return s + "!"; })
.execute(" hello world ", s -> { System.out.println("[Terminal] " + s); return s; });
System.out.println("Result: " + result);Asynchronous Chain with CompletableFuture
AsyncChain<OrderResult> chain = new AsyncChain<>(() -> {
System.out.println("[Init] Create order context");
return new OrderResult(true, "Initial", "ORD001", BigDecimal.ZERO);
});
OrderResult result = chain
.thenApplyAsync(r -> { System.out.println("[Async 1] Stock check"); sleep(100); return r; })
.thenApplyAsync(r -> { System.out.println("[Async 2] Payment validation"); sleep(100); return r; })
.thenApplyIfAsync(r -> r.isSuccess(), r -> { System.out.println("[Async 3] Risk review"); sleep(100); return new OrderResult(true, "Approved", r.getOrderId(), BigDecimal.valueOf(100)); })
.exceptionally(e -> { System.err.println("Error: " + e.getMessage()); return new OrderResult(false, "Failed", null, BigDecimal.ZERO); })
.get(5, TimeUnit.SECONDS);
System.out.println("Final result: " + result);Performance Optimizations & Best Practices
Avoid overly long chains : group related handlers (validation, business, post‑processing) to keep the chain manageable.
Short‑circuiting : let a handler return a failure result early to stop further processing.
Cache results : use a cache (e.g., Guava) for idempotent operations and store successful outcomes.
Monitoring & logging : wrap handlers with metrics (success/failure counters, duration timers) and emit warnings for long‑running handlers.
Common Pitfalls & Solutions
Forgot to pass to the next handler
// Wrong – returns without delegating
public Result handle(Request request, RequestChain chain) { System.out.println("Processing..."); return Result.success(); }
// Correct – delegate to the chain
public Result handle(Request request, RequestChain chain) { System.out.println("Processing..."); return chain.proceed(request); }Circular references
// Wrong – creates a loop a → b → c → a
// Correct – ensure the last handler has no next referenceThread‑safety issues
// Wrong – mutable shared state (e.g., currentIndex) without synchronization
// Correct – keep handlers stateless or use ThreadLocal where neededImproper exception handling
// Wrong – swallow all exceptions and return success
// Correct – log business exceptions, return a failure result, and re‑throw system exceptionsConclusion
The Chain of Responsibility pattern is a powerful tool for scenarios that require multi‑step, pluggable, sequential processing. Its key benefits are decoupling, flexibility, single‑responsibility, and extensibility. It fits well for approval workflows, request filtering, logging pipelines, and interceptor chains, while being mindful of chain length, short‑circuiting, caching, monitoring, thread safety, and proper error handling.
References
Chain of Responsibility – Wikipedia
Spring Security Filter Chain documentation
Java Servlet Filter specification
Design Patterns – Chain of Responsibility chapter
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.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
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.
