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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Applying the Chain of Responsibility Pattern for Multi‑Level Product Validation and Workflow in Java

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendChain of ResponsibilityJavaspringdesign pattern
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

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.