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.
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.javaOrder 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
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.
