Mastering the Chain of Responsibility Pattern for Scalable Backend Validation
This article explains the Chain of Responsibility design pattern, outlines its typical application scenarios, and demonstrates two practical implementations—a multi‑step product creation validation and an expense‑approval workflow—showing how to configure, assemble, and execute handler chains dynamically in Java.
Chain of Responsibility Pattern Overview
The Chain of Responsibility pattern assembles multiple operations into a linked chain where a request traverses the chain; each node (handler) can process the request or pass it to the next handler.
Application Scenarios
Typical uses include:
Operations that require a series of validations before execution.
Workflow processing where tasks are handled step by step across organizational levels.
Case 1: Multi‑Level Validation for Product Creation
Creating a product involves three steps: create, validate parameters, and save. Validation is split into several checks (null values, price, stock) that form a pipeline.
Pseudocode
if (parameter validation fails) return failure; else save product;When business requirements grow, validation logic becomes tangled and hard to maintain. Refactoring with the Chain of Responsibility separates each validation into its own handler.
Optimized Implementation
Each validation step becomes a concrete handler extending AbstractCheckHandler. Handlers are registered as Spring beans, linked via a configuration object, and executed in order.
@Data @Builder public class ProductVO { Long skuId; String skuName; String Path; BigDecimal price; Integer stock; } @Component public abstract class AbstractCheckHandler { protected AbstractCheckHandler nextHandler; protected ProductCheckHandlerConfig config; public abstract Result handle(ProductVO param); protected Result next(ProductVO param) { if (Objects.isNull(nextHandler)) return Result.success(); return nextHandler.handle(param); } }Concrete handlers such as NullValueCheckHandler, PriceCheckHandler, and StockCheckHandler override handle() to perform specific checks and invoke next(param) when successful.
@Component public class NullValueCheckHandler extends AbstractCheckHandler { @Override public Result handle(ProductVO param) { if (super.getConfig().getDown()) return super.next(param); if (Objects.isNull(param) || Objects.isNull(param.getSkuId())) return Result.failure(ErrorCode.PARAM_NULL_ERROR); // other checks ... return super.next(param); } }The client initiates the chain with HandlerClient.executeChain(handler, param), which returns a failure if any handler rejects the request.
public class HandlerClient { public static Result executeChain(AbstractCheckHandler handler, ProductVO param) { Result handlerResult = handler.handle(param); if (!handlerResult.isSuccess()) { System.out.println("HandlerClient execution failed: " + handlerResult); return handlerResult; } return Result.success(); } }Configuration is stored as JSON (simulating a config center) and parsed into ProductCheckHandlerConfig, which defines each handler’s bean name, next handler, and optional downgrade flag.
String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}"; ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig.class);The getHandler(config) method retrieves the bean from a Map<String, AbstractCheckHandler>, sets its configuration, and recursively links the next handler.
private AbstractCheckHandler getHandler(ProductCheckHandlerConfig config) { if (Objects.isNull(config) || StringUtils.isBlank(config.getHandler())) return null; AbstractCheckHandler handler = handlerMap.get(config.getHandler()); if (Objects.isNull(handler)) return null; handler.setConfig(config); handler.setNextHandler(getHandler(config.getNext())); return handler; }Case 2: Expense Reimbursement Workflow
A similar chain handles approval levels based on the reimbursement amount: tier‑3 manager, tier‑2 manager, and tier‑1 manager, each with configurable amount ranges and next approver.
The abstract flow handler defines an approve() method, and concrete handlers implement department‑specific logic. Configuration can be changed at runtime to adjust approval limits.
Advantages and Drawbacks
Advantages: decouples request processing, promotes single‑responsibility, and enables dynamic reconfiguration. Drawbacks: can lead to long chains and potential performance overhead; recursive linking must include termination conditions to avoid infinite loops.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
