Backend Development 21 min read

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.

Top Architecture Tech Stack
Top Architecture Tech Stack
Top Architecture Tech Stack
Implementing the Chain of Responsibility Pattern for Product Validation and Workflow Approval in Java

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 ---

chain of responsibilityJavaSpringValidationdesign pattern
Top Architecture Tech Stack
Written by

Top Architecture Tech Stack

Sharing Java and Python tech insights, with occasional practical development tool tips.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.