Applying the Chain of Responsibility Pattern for Multi‑Level Product Validation and Workflow in Java
This article explains the Chain of Responsibility design pattern, demonstrates its use in multi‑step product creation validation and expense‑approval workflows with concrete Java/Spring code, and discusses configuration, dynamic composition, advantages, drawbacks, and testing scenarios.
Hello everyone, I’m Chen.
The Chain of Responsibility pattern assembles multiple operations into a processing chain. A request travels along the chain; each node is a handler that can either process the request or pass it to the next handler.
Application Scenarios
The pattern is commonly used in two situations:
Operations that require a series of validations before execution.
Workflows where tasks are processed step by step in an organization.
Below are two case studies to illustrate the pattern.
Case 1: Multi‑Level Product Validation
Creating a product involves three steps: ① create product, ② validate product parameters, ③ save product. Validation itself includes required‑field checks, specification checks, price checks, stock checks, etc., forming a pipeline that must be passed before the product can be created.
When the validation logic grows, the code becomes bulky and hard to maintain. By refactoring with the Chain of Responsibility, each validation step becomes an independent handler, improving reusability and separation of concerns.
UML Overview
AbstractCheckHandler – the abstract handler class. It defines three subclasses:
NullValueCheckHandler – checks for null values.
PriceCheckHandler – validates price.
StockCheckHandler – validates stock.
The abstract class provides the handle() method (to be overridden by subclasses) and a protected next(ProductVO param) method that forwards the request to the next handler.
Each handler holds a ProductCheckHandlerConfig object that specifies the bean name, the next handler, and optional downgrade flags.
The client invokes the chain via HandlerClient.executeChain(), which returns the result of the whole chain.
Product Parameter Object (ProductVO)
/**
* Product object
*/
@Data
@Builder
public class ProductVO {
/** SKU, unique */
private Long skuId;
/** Product name */
private String skuName;
/** Image path */
private String Path;
/** Price */
private BigDecimal price;
/** Stock */
private Integer stock;
}AbstractCheckHandler (abstract class)
/**
* Abstract handler
*/
@Component
public abstract class AbstractCheckHandler {
@Getter @Setter
protected AbstractCheckHandler nextHandler;
@Getter @Setter
protected ProductCheckHandlerConfig config;
/**
* Handler execution method
*/
public abstract Result handle(ProductVO param);
/**
* Pass request to next handler
*/
protected Result next(ProductVO param) {
if (Objects.isNull(nextHandler)) {
return Result.success();
}
return nextHandler.handle(param);
}
}Configuration Class
/**
* Handler configuration class
*/
@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
/** Bean name of the handler */
private String handler;
/** Next handler configuration */
private ProductCheckHandlerConfig next;
/** Whether the handler is downgraded */
private Boolean down = Boolean.FALSE;
}Concrete Handlers
NullValueCheckHandler – validates required fields and supports downgrade logic.
@Component
public class NullValueCheckHandler extends AbstractCheckHandler {
@Override
public Result handle(ProductVO param) {
System.out.println("Null value check handler start...");
if (super.getConfig().getDown()) {
System.out.println("Null value check handler downgraded, skipping...");
return super.next(param);
}
if (Objects.isNull(param)) return Result.failure(ErrorCode.PARAM_NULL_ERROR);
if (Objects.isNull(param.getSkuId())) return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
if (Objects.isNull(param.getPrice())) return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
if (Objects.isNull(param.getStock())) return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
System.out.println("Null value check passed...");
return super.next(param);
}
}PriceCheckHandler – ensures price > 0.
@Component
public class PriceCheckHandler extends AbstractCheckHandler {
@Override
public Result handle(ProductVO param) {
System.out.println("Price check handler start...");
boolean illegalPrice = param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
if (illegalPrice) return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
System.out.println("Price check passed...");
return super.next(param);
}
}StockCheckHandler – ensures stock >= 0.
@Component
public class StockCheckHandler extends AbstractCheckHandler {
@Override
public Result handle(ProductVO param) {
System.out.println("Stock check handler start...");
boolean illegalStock = param.getStock() < 0;
if (illegalStock) return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
System.out.println("Stock check passed...");
return super.next(param);
}
}HandlerClient
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient chain execution failed: " + handlerResult);
return handlerResult;
}
return Result.success();
}
}Creating the Product
@Test
public Result createProduct(ProductVO param) {
// Parameter validation using the chain
Result paramCheckResult = this.paramCheck(param);
if (!paramCheckResult.isSuccess()) {
return paramCheckResult;
}
// Save product
return this.saveProduct(param);
}The paramCheck() method reads the handler configuration (normally from a configuration centre), builds the chain recursively, and executes it via HandlerClient.executeChain().
private ProductCheckHandlerConfig getHandlerConfigFile() {
String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
return JSON.parseObject(configJson, ProductCheckHandlerConfig.class);
}Handler retrieval uses a Spring‑injected Map<String, AbstractCheckHandler> handlerMap to obtain beans by name, sets the configuration, and recursively links the next handler.
@Resource
private Map<String, AbstractCheckHandler> handlerMap;
private AbstractCheckHandler getHandler(ProductCheckHandlerConfig config) {
if (Objects.isNull(config) || StringUtils.isBlank(config.getHandler())) {
return null;
}
AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler());
if (Objects.isNull(abstractCheckHandler)) {
return null;
}
abstractCheckHandler.setConfig(config);
abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));
return abstractCheckHandler;
}After the chain is built, HandlerClient.executeChain(handler, param) runs the handlers in order; any failure stops the chain and returns the error.
Case 2: Workflow – Expense Reimbursement Approval
Three approval levels are defined based on the reimbursement amount: < 1,000 ¥ (level‑3 only), 1,000‑5,000 ¥ (level‑3 + level‑2), 5,000‑10,000 ¥ (level‑3 + level‑2 + level‑1). The same Chain of Responsibility structure is used, with AbstractFlowHandler defining an approve() method that concrete handlers override.
The configuration class stores the approver, amount limits, and the next handler, allowing dynamic changes without code modifications.
Pros and Cons of the Chain of Responsibility
Source Code
Repository: https://github.com/rongtao7/MyNotes
Original source: blog.csdn.net/rongtaoup/article/details/122638812
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.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
