Mastering the Chain of Responsibility Pattern for Scalable Product Validation in Java

This article explains the Chain of Responsibility design pattern, shows how to apply it to multi‑step product creation validation and expense‑approval workflows, walks through abstract and concrete handler implementations, dynamic configuration, and client execution with full Java code examples.

Senior Brother's Insights
Senior Brother's Insights
Senior Brother's Insights
Mastering the Chain of Responsibility Pattern for Scalable Product Validation in Java

Chain of Responsibility pattern

The Chain of Responsibility (CoR) pattern links a series of handler objects into a chain. A request is passed along the chain; each handler can either process the request or forward it to the next handler. This decouples the sender of a request from its receivers and allows dynamic composition of processing steps.

Typical application scenarios

When an operation requires a sequence of validations before the main business logic is executed.

When a workflow consists of several approval or processing stages that may be reordered or extended.

Example 1: Multi‑step product validation

Creating a product involves three logical steps: create the product entity, validate its parameters, and persist it. Validation itself consists of several independent checks (required fields, price, stock, etc.). Using CoR each check is encapsulated in its own handler, making the validation logic reusable and easy to extend.

Domain model

@Data
@Builder
public class ProductVO {
    private Long skuId;          // unique identifier
    private String skuName;      // product name
    private String path;        // image URL
    private BigDecimal price;   // price, must be > 0
    private Integer stock;      // stock, must be >= 0
}

Abstract handler

@Component
public abstract class AbstractCheckHandler {
    @Getter @Setter
    protected AbstractCheckHandler nextHandler;
    @Getter @Setter
    protected ProductCheckHandlerConfig config;

    /** Each concrete handler implements its own validation logic. */
    public abstract Result handle(ProductVO param);

    /** Calls the next handler in the chain, or returns success if none. */
    protected Result next(ProductVO param) {
        return (nextHandler == null) ? Result.success() : nextHandler.handle(param);
    }
}

Concrete handlers

NullValueCheckHandler – verifies that the request object and mandatory fields are not null.

PriceCheckHandler – ensures price > 0.

StockCheckHandler – ensures stock >= 0.

@Component
public class NullValueCheckHandler extends AbstractCheckHandler {
    @Override
    public Result handle(ProductVO param) {
        if (config.getDown()) { // optional downgrade flag
            return super.next(param);
        }
        if (param == null) {
            return Result.failure(ErrorCode.PARAM_NULL_ERROR);
        }
        if (param.getSkuId() == null) {
            return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
        }
        if (param.getPrice() == null) {
            return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
        }
        if (param.getStock() == null) {
            return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
        }
        return super.next(param);
    }
}
@Component
public class PriceCheckHandler extends AbstractCheckHandler {
    @Override
    public Result handle(ProductVO param) {
        boolean illegal = param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
        if (illegal) {
            return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
        }
        return super.next(param);
    }
}
@Component
public class StockCheckHandler extends AbstractCheckHandler {
    @Override
    public Result handle(ProductVO param) {
        if (param.getStock() < 0) {
            return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
        }
        return super.next(param);
    }
}

Configuration model

@Data @AllArgsConstructor
public class ProductCheckHandlerConfig {
    /** Spring bean name of the handler */
    private String handler;
    /** Next handler configuration (may be null) */
    private ProductCheckHandlerConfig next;
    /** Optional downgrade flag – when true the handler is skipped */
    private Boolean down = Boolean.FALSE;
}

The configuration is stored as JSON (e.g., in Nacos, Ducc, or any configuration centre) and parsed into ProductCheckHandlerConfig objects:

private ProductCheckHandlerConfig getHandlerConfigFile() {
    String json = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
    return JSON.parseObject(json, ProductCheckHandlerConfig.class);
}

Dynamic chain assembly

@Resource
private Map<String, AbstractCheckHandler> handlerMap; // injected by Spring

private AbstractCheckHandler getHandler(ProductCheckHandlerConfig cfg) {
    if (cfg == null || StringUtils.isBlank(cfg.getHandler())) {
        return null;
    }
    AbstractCheckHandler handler = handlerMap.get(cfg.getHandler());
    if (handler == null) {
        return null;
    }
    handler.setConfig(cfg);
    handler.setNextHandler(getHandler(cfg.getNext())); // recursive linking
    return handler;
}

Client execution

public class HandlerClient {
    public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
        Result r = handler.handle(param);
        return r.isSuccess() ? Result.success() : r;
    }
}

Service method using the chain

public Result createProduct(ProductVO param) {
    Result check = paramCheck(param);
    if (!check.isSuccess()) {
        return check;
    }
    return saveProduct(param); // actual persistence logic
}

private Result paramCheck(ProductVO param) {
    ProductCheckHandlerConfig cfg = getHandlerConfigFile();
    AbstractCheckHandler chain = getHandler(cfg);
    return HandlerClient.executeChain(chain, param);
}

Four test scenarios illustrate the behaviour:

Missing skuId – the null‑value handler stops the chain and returns PARAM_SKU_NULL_ERROR.

Negative price – the price handler stops the chain and returns PARAM_PRICE_ILLEGAL_ERROR.

Negative stock – the stock handler stops the chain and returns PARAM_STOCK_ILLEGAL_ERROR.

All fields valid – the chain completes and the product is persisted.

Example 2: Expense‑reimbursement workflow

A second CoR example models a multi‑level approval process. The abstract class AbstractFlowHandler defines an approve() method. Concrete handlers ( FirstLevelHandler, SecondLevelHandler, ThirdLevelHandler) implement level‑specific rules (e.g., maximum amount they can approve). A JSON configuration specifies the first handler, the amount limits, and the next handler, allowing the approval chain to be re‑configured without code changes.

Advantages and disadvantages

Decouples request senders from concrete receivers.

Adding, removing or re‑ordering handlers is straightforward.

Chain structure can be driven by external configuration, enabling runtime changes.

Long chains may introduce noticeable latency.

Control flow is implicit, which can make debugging harder.

Incorrect configuration can cause infinite recursion or broken chains.

Chain of Responsibility pros and cons
Chain of Responsibility pros and cons
Chain of Responsibility drawbacks
Chain of Responsibility drawbacks
Note: The configuration object stores only the Spring bean names; the actual handler instances are resolved from the handlerMap at runtime.
Chain of Responsibilitydesign-patternsJavaBackend DevelopmentSpring
Senior Brother's Insights
Written by

Senior Brother's Insights

A public account focused on workplace, career growth, team management, and self-improvement. The author is the writer of books including 'SpringBoot Technology Insider' and 'Drools 8 Rule Engine: Core Technology and Practice'.

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.