Mastering the Chain of Responsibility in Spring Boot for Dynamic Workflow Orchestration

This article explains the Chain of Responsibility pattern, outlines its core components, demonstrates a complete Spring Boot order‑processing example with concrete handlers, and shares advanced techniques such as dynamic handler configuration and asynchronous processing, concluding with practical best‑practice tips.

Senior Xiao Ying
Senior Xiao Ying
Senior Xiao Ying
Mastering the Chain of Responsibility in Spring Boot for Dynamic Workflow Orchestration

Responsibility Chain Pattern

Responsibility Chain is a behavioral design pattern that allows multiple objects a chance to handle a request, decoupling the sender from the receiver. Objects are linked into a chain; the request traverses the chain until an object processes it.

Core Roles

Handler (abstract) : defines the request‑handling interface and holds a reference to the next handler.

ConcreteHandler : implements the handling logic, decides whether to process the request or forward it.

Client : builds the chain and sends the request to the head of the chain.

Advantages in Spring Boot

Spring’s dependency injection automatically registers handlers.

Handler order can be adjusted dynamically.

Easy to extend and maintain.

Aligns with the Open‑Closed Principle.

Example: Order Processing System

Project structure

src/main/java/com/example/chain/
├── controller/
│   └── OrderController.java
├── handler/
│   ├── OrderHandler.java
│   ├── abstract/
│   │   └── AbstractOrderHandler.java
│   ├── concrete/
│   │   ├── ValidationHandler.java
│   │   ├── InventoryHandler.java
│   │   ├── PriceCalculationHandler.java
│   │   ├── PaymentHandler.java
│   │   └── NotificationHandler.java
│   └── context/
│       └── OrderContext.java
├── config/
│   └── HandlerConfig.java
└── entity/
    └── Order.java

Order Entity

package com.example.chain.entity;

import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Data
public class Order {
    private Long orderId;
    private String orderNo;
    private Long userId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private BigDecimal finalAmount;
    private BigDecimal discountAmount;
    private Integer status; // 0: pending, 1: validated, 2: stock locked, 3: priced, 4: paid, 5: completed
    private String paymentMethod;
    private LocalDateTime createTime;
    private LocalDateTime payTime;
    private String errorMessage;

    @Data
    public static class OrderItem {
        private Long productId;
        private String productName;
        private Integer quantity;
        private BigDecimal price;
        private BigDecimal subtotal;
    }
}

Processing Context

package com.example.chain.handler.context;

import com.example.chain.entity.Order;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;

/** Context object passed through the chain. */
@Data
public class OrderContext {
    // Current order
    private Order order;
    // Continue flag – true to keep processing, false to stop
    private boolean continueProcessing = true;
    // Temporary data shared among handlers
    private Map<String, Object> attributes = new HashMap<>();

    public OrderContext(Order order) {
        this.order = order;
    }

    public void setAttribute(String key, Object value) {
        attributes.put(key, value);
    }

    public Object getAttribute(String key) {
        return attributes.get(key);
    }

    public void interruptProcessing(String errorMessage) {
        this.continueProcessing = false;
        this.order.setErrorMessage(errorMessage);
    }
}

Abstract Handler

package com.example.chain.handler.abstracts;

import com.example.chain.handler.context.OrderContext;
import org.springframework.stereotype.Component;

@Component
public abstract class AbstractOrderHandler {
    // Next handler in the chain
    protected AbstractOrderHandler nextHandler;

    /** Set the next handler */
    public void setNextHandler(AbstractOrderHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    /** Template method that controls the chain flow */
    public final void handle(OrderContext context) {
        // Stop if processing has been interrupted
        if (!context.isContinueProcessing()) {
            return;
        }
        // Current handler processes the request
        process(context);
        // Continue to the next handler if still allowed
        if (context.isContinueProcessing() && nextHandler != null) {
            nextHandler.handle(context);
        }
    }

    /** Concrete handlers implement this */
    protected abstract void process(OrderContext context);

    /** Order for sorting handlers */
    public abstract int getOrder();
}

Concrete Handler – Validation

package com.example.chain.handler.concrete;

import com.example.chain.entity.Order;
import com.example.chain.handler.abstracts.AbstractOrderHandler;
import com.example.chain.handler.context.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ValidationHandler extends AbstractOrderHandler {
    @Override
    protected void process(OrderContext context) {
        Order order = context.getOrder();
        log.info("Starting validation for order: {}", order.getOrderNo());
        // Basic checks
        if (order.getUserId() == null) {
            context.interruptProcessing("User ID cannot be null");
            return;
        }
        if (order.getItems() == null || order.getItems().isEmpty()) {
            context.interruptProcessing("Order items cannot be empty");
            return;
        }
        // Validate each item
        for (Order.OrderItem item : order.getItems()) {
            if (item.getQuantity() <= 0) {
                context.interruptProcessing("Product " + item.getProductId() + " has invalid quantity");
                return;
            }
            if (item.getPrice().compareTo(java.math.BigDecimal.ZERO) <= 0) {
                context.interruptProcessing("Product " + item.getProductId() + " has invalid price");
                return;
            }
            // Compute subtotal
            item.setSubtotal(item.getPrice().multiply(new java.math.BigDecimal(item.getQuantity())));
        }
        // Update status
        order.setStatus(1);
        log.info("Validation passed for order: {}", order.getOrderNo());
    }

    @Override
    public int getOrder() {
        return 1; // first to execute
    }
}

Concrete Handler – Inventory Check

package com.example.chain.handler.concrete;

import com.example.chain.entity.Order;
import com.example.chain.handler.abstracts.AbstractOrderHandler;
import com.example.chain.handler.context.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class InventoryHandler extends AbstractOrderHandler {
    // Simulated inventory data
    private static final Map<Long, Integer> INVENTORY = new HashMap<>() {{
        put(1001L, 10);
        put(1002L, 5);
        put(1003L, 0);
    }};

    @Override
    protected void process(OrderContext context) {
        Order order = context.getOrder();
        log.info("Checking inventory for order: {}", order.getOrderNo());
        // Verify stock for each item
        for (Order.OrderItem item : order.getItems()) {
            Integer available = INVENTORY.getOrDefault(item.getProductId(), 0);
            if (available < item.getQuantity()) {
                context.interruptProcessing(String.format(
                    "Product %s stock insufficient: need %d, available %d",
                    item.getProductId(), item.getQuantity(), available));
                return;
            }
        }
        // Simulate stock lock
        for (Order.OrderItem item : order.getItems()) {
            Integer current = INVENTORY.get(item.getProductId());
            INVENTORY.put(item.getProductId(), current - item.getQuantity());
            log.info("Locked stock: product {}, qty {}, remaining {}",
                item.getProductId(), item.getQuantity(), current - item.getQuantity());
        }
        order.setStatus(2);
        log.info("Inventory check passed and locked for order: {}", order.getOrderNo());
    }

    @Override
    public int getOrder() {
        return 2; // second to execute
    }
}

Advanced Optimizations

Dynamic Handler Configuration

@Component
public class DynamicHandlerChain {
    @Autowired
    private ApplicationContext applicationContext;

    /** Build the chain based on a list of bean names */
    public AbstractOrderHandler buildChain(List<String> handlerNames) {
        List<AbstractOrderHandler> handlers = new ArrayList<>();
        for (String name : handlerNames) {
            AbstractOrderHandler handler = (AbstractOrderHandler) applicationContext.getBean(name);
            handlers.add(handler);
        }
        // Link the handlers
        for (int i = 0; i < handlers.size() - 1; i++) {
            handlers.get(i).setNextHandler(handlers.get(i + 1));
        }
        return handlers.isEmpty() ? null : handlers.get(0);
    }
}

Asynchronous Processing

@Component
public class AsyncOrderHandler extends AbstractOrderHandler {
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    @Override
    protected void process(OrderContext context) {
        // Run time‑consuming steps asynchronously
        CompletableFuture.runAsync(() -> {
            // async logic here
        }, taskExecutor);
    }
}

Practical Takeaways

Choose appropriate scenarios :the pattern fits fixed‑flow but dynamically adjustable processes such as approval pipelines or data‑processing chains.

Mind performance :each request traverses the whole chain, so keep the chain reasonably short.

Handle exceptions locally :each handler should catch its own errors to avoid breaking the entire workflow.

Log execution :record handler entry/exit to aid debugging and monitoring.

Configure flexibly :use configuration files or a database to reorder handlers without code changes.

Cover with tests :write unit tests for every concrete handler to guarantee correctness.

Chain of Responsibility diagram
Chain of Responsibility diagram
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.

Chain of ResponsibilityJavaSpring Bootdesign-patternDynamic Workflow
Senior Xiao Ying
Written by

Senior Xiao Ying

Dedicated to sharing Java backend technical experience and original tutorials, offering career transition advice and resume editing. Recognized as a rising star in CSDN's Java backend community and ranked Top 3 in the 2022 New Star Program for Java backend.

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.