Implementing the Chain of Responsibility Pattern for Product Validation and Workflow Approval in Java
This article explains the Chain of Responsibility design pattern, demonstrates its application in multi‑step product validation and expense‑approval workflows with detailed Java code, configuration handling, Spring bean injection, and discusses its advantages and drawbacks.
The Chain of Responsibility pattern assembles multiple operations into a processing chain where a request travels through each handler, and each handler can either process the request or pass it to the next handler.
Application Scenarios
The pattern is commonly used in two situations: (1) operations that require a series of validations before execution, and (2) workflow processing where tasks are handled level by level.
Case 1: Multi‑Level Validation for Product Creation
Creating a product involves three steps: ① create product, ② validate product parameters, ③ save product. The validation step itself consists of several checks such as required‑field validation, specification validation, price validation, and stock validation, forming a pipeline.
Pseudo‑code: The product creation process passes through a series of parameter checks; if any check fails, the process returns an error immediately, otherwise the product is saved.
/**
* 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;
}The abstract handler class defines common behavior and properties for all concrete handlers.
@Component
public abstract class AbstractCheckHandler {
@Getter @Setter
protected AbstractCheckHandler nextHandler;
@Setter @Getter
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);
}
}The configuration class stores the chain definition and can be loaded from a configuration center.
@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
private String handler; // bean name
private ProductCheckHandlerConfig next; // next node
private Boolean down = Boolean.FALSE; // downgrade flag
}Concrete handlers implement specific validation logic.
@Component
public class NullValueCheckHandler extends AbstractCheckHandler {
@Override
public Result handle(ProductVO param) {
System.out.println("Null value check start...");
if (super.getConfig().getDown()) {
System.out.println("Null check 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);
}
} @Component
public class PriceCheckHandler extends AbstractCheckHandler {
@Override
public Result handle(ProductVO param) {
System.out.println("Price check 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);
}
} @Component
public class StockCheckHandler extends AbstractCheckHandler {
@Override
public Result handle(ProductVO param) {
System.out.println("Stock check 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);
}
}The client class triggers the whole chain.
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();
}
}The product creation method delegates validation to the chain and only saves the product when all checks succeed.
@Test
public Result createProduct(ProductVO param) {
Result paramCheckResult = this.paramCheck(param);
if (!paramCheckResult.isSuccess()) {
return paramCheckResult;
}
return this.saveProduct(param);
}
private Result paramCheck(ProductVO param) {
ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile();
AbstractCheckHandler handler = this.getHandler(handlerConfig);
Result executeChainResult = HandlerClient.executeChain(handler, param);
if (!executeChainResult.isSuccess()) {
System.out.println("Create product failed...");
return executeChainResult;
}
return Result.success();
}
private ProductCheckHandlerConfig getHandlerConfigFile() {
String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
return JSON.parseObject(configJson, ProductCheckHandlerConfig.class);
}
@Resource
private Map
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;
}Test cases demonstrate how the chain stops at the first failing handler (null value, illegal price, or illegal stock) and returns the corresponding error, while a fully valid product passes all handlers and is saved.
Case 2: Workflow – Expense Reimbursement Approval
A colleague submits an expense report; the approval flow depends on the amount: ≤ 1000 ¥ requires only third‑level manager approval, 1000–5000 ¥ adds second‑level approval, and 5000–10000 ¥ also needs first‑level approval.
The abstract flow handler defines an approve() method; concrete handlers for first, second, and third level managers extend it and implement their own approval logic, forming a chain similar to the product validation example.
Advantages and Disadvantages of the Chain of Responsibility
In summary, the pattern decouples validation or approval logic, improves reusability, and allows dynamic configuration, but excessive recursion can lead to complexity and potential stack overflow if not properly bounded.
--- End of article ---
Top Architecture Tech Stack
Sharing Java and Python tech insights, with occasional practical development tool tips.
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.