Implementing Order Workflow with Spring Statemachine and Persistent State Storage
This article explains the fundamentals of finite‑state machines, introduces Spring Statemachine, demonstrates how to model an order lifecycle, configure persistence with in‑memory and Redis stores, and provides complete Java code for enums, configuration, services, controllers, listeners, testing, and exception‑handling improvements.
1. What Is a State Machine
A state machine (finite‑state machine) is a mathematical model that describes a set of states and the transitions between them, such as an automatic door with open and closed states.
1.1 What Is a State
Real‑world objects have distinct states; a state machine abstracts these into a diagram that defines how inputs (events) cause state transitions.
1.2 Four Core Concepts
State : the condition of the object (e.g., open , closed ).
Event : the trigger that causes a transition (e.g., pressing the open button).
Action : the operation executed when an event occurs (often a method call).
Transition : the movement from one state to another.
1.3 Finite‑State Machine (FSM)
An FSM consists of a finite set of states, an initial state, inputs, and a transition function that determines the next state based on the current state and input.
2. State Machine Diagram
For an order process, the diagram includes six elements: start, end, current state, target state, action, and condition. Example: WAIT_PAYMENT → (pay) → WAIT_DELIVER → (deliver) → WAIT_RECEIVE → (receive) → FINISH .
3. Spring Statemachine
3.1 Overview
Spring Statemachine provides a framework for using state‑machine concepts within Spring applications, offering simple flat machines, hierarchical structures, regions, triggers, guards, actions, a generator pattern, Zookeeper‑based distributed machines, listeners, and integration with Spring IoC.
3.2 Quick Start
Define a table tb_order to store order data:
CREATE TABLE `tb_order` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_code` varchar(128) DEFAULT NULL COMMENT '订单编码',
`status` smallint DEFAULT NULL COMMENT '订单状态',
`name` varchar(64) DEFAULT NULL COMMENT '订单名称',
`price` decimal(12,2) DEFAULT NULL COMMENT '价格',
`delete_flag` tinyint(2) NOT NULL DEFAULT '0' COMMENT '删除标记,0未删除 1已删除',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间',
`create_user_code` varchar(32) DEFAULT NULL COMMENT '创建人',
`update_user_code` varchar(32) DEFAULT NULL COMMENT '更新人',
`version` int DEFAULT '0' COMMENT '版本号',
`remark` varchar(64) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='订单表';Add the required Maven dependencies:
org.springframework.statemachine
spring-statemachine-redis
1.2.9.RELEASE
org.springframework.statemachine
spring-statemachine-starter
2.0.1.RELEASE3.3 Defining States and Events
public enum OrderStatus {
WAIT_PAYMENT(1, "待支付"),
WAIT_DELIVER(2, "待发货"),
WAIT_RECEIVE(3, "待收货"),
FINISH(4, "已完成");
private Integer key;
private String desc;
// getters and utility methods omitted for brevity
} public enum OrderStatusChangeEvent { PAYED, DELIVERY, RECEIVED; }3.4 Configuring the State Machine
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter
{
@Override
public void configure(StateMachineStateConfigurer
states) throws Exception {
states.withStates()
.initial(OrderStatus.WAIT_PAYMENT)
.states(EnumSet.allOf(OrderStatus.class));
}
@Override
public void configure(StateMachineTransitionConfigurer
transitions) throws Exception {
transitions.withExternal().source(OrderStatus.WAIT_PAYMENT).target(OrderStatus.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED)
.and()
.withExternal().source(OrderStatus.WAIT_DELIVER).target(OrderStatus.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY)
.and()
.withExternal().source(OrderStatus.WAIT_RECEIVE).target(OrderStatus.FINISH).event(OrderStatusChangeEvent.RECEIVED);
}
}3.5 Persistence
Memory persistence bean:
@Bean(name = "stateMachineMemPersister")
public static StateMachinePersister getPersister() {
return new DefaultStateMachinePersister(new StateMachinePersist() {
private Map map = new HashMap();
@Override
public void write(StateMachineContext context, Object contextObj) {
map.put(contextObj, context);
}
@Override
public StateMachineContext read(Object contextObj) {
return (StateMachineContext) map.get(contextObj);
}
});
}Redis persistence bean (requires Redis connection factory):
@Bean(name = "stateMachineRedisPersister")
public RedisStateMachinePersister
getRedisPersister() {
RedisStateMachineContextRepository
repository = new RedisStateMachineContextRepository<>(redisConnectionFactory);
RepositoryStateMachinePersist
p = new RepositoryStateMachinePersist<>(repository);
return new RedisStateMachinePersister<>(p);
}4. Business Layer
Controller endpoints for creating orders and driving the workflow:
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource private OrderService orderService;
@RequestMapping("/create")
public String create(@RequestBody Order order) { orderService.create(order); return "success"; }
@RequestMapping("/pay")
public String pay(@RequestParam("id") Long id) { orderService.pay(id); return "success"; }
@RequestMapping("/deliver")
public String deliver(@RequestParam("id") Long id) { orderService.deliver(id); return "success"; }
@RequestMapping("/receive")
public String receive(@RequestParam("id") Long id) { orderService.receive(id); return "success"; }
}Service implementation uses the state machine, persists state, and logs actions:
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl
implements OrderService {
@Resource private StateMachine
orderStateMachine;
@Resource private StateMachinePersister
stateMachineMemPersister;
@Resource private OrderMapper orderMapper;
public Order create(Order order) { order.setStatus(OrderStatus.WAIT_PAYMENT.getKey()); orderMapper.insert(order); return order; }
public Order pay(Long id) { Order order = orderMapper.selectById(id); if (!sendEvent(OrderStatusChangeEvent.PAYED, order, CommonConstants.payTransition)) { throw new RuntimeException("支付失败, 订单状态异常"); } return order; }
// deliver and receive methods omitted for brevity
private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order, String key) {
boolean result = false;
try {
orderStateMachine.start();
stateMachineMemPersister.restore(orderStateMachine, String.valueOf(order.getId()));
Message message = MessageBuilder.withPayload(changeEvent).setHeader("order", order).build();
result = orderStateMachine.sendEvent(message);
if (!result) return false;
Integer flag = (Integer) orderStateMachine.getExtendedState().getVariables().get(key + order.getId());
orderStateMachine.getExtendedState().getVariables().remove(key + order.getId());
if (Objects.equals(1, flag)) {
stateMachineMemPersister.persist(orderStateMachine, String.valueOf(order.getId()));
} else {
return false;
}
} catch (Exception e) {
// log error
} finally {
orderStateMachine.stop();
}
return result;
}
}4.1 Listener with Transactional Transitions
@Component("orderStateListener")
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListenerImpl {
@Resource private OrderMapper orderMapper;
@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
@Transactional(rollbackFor = Exception.class)
@LogResult(key = CommonConstants.payTransition)
public void payTransition(Message
message) {
Order order = (Order) message.getHeaders().get("order");
order.setStatus(OrderStatus.WAIT_DELIVER.getKey());
orderMapper.updateById(order);
if (Objects.equals(order.getName(), "A")) { throw new RuntimeException("执行业务异常"); }
}
// deliverTransition and receiveTransition omitted for brevity
}5. Testing the Workflow
Typical API calls:
Create order: POST http://localhost:8084/order/create
Pay order: GET http://localhost:8084/order/pay?id=2
Deliver order: GET http://localhost:8084/order/deliver?id=2
Receive order: GET http://localhost:8084/order/receive?id=2
Repeating a transition (e.g., paying twice) results in an error because the state machine prevents illegal state changes.
6. Problems and Solutions
6.1 State Machine Swallows Exceptions
Calling orderStateMachine.sendEvent(message) always returns true , even when a listener throws an exception. The exception is consumed by the state machine, making it impossible for the caller to detect failure.
6.2 Using Extended State to Propagate Results
Listeners store a success flag (1) or failure flag (0) in orderStateMachine.getExtendedState().getVariables() . The sender reads this flag after sendEvent and decides whether to persist the state.
6.3 AOP Wrapper for Listeners
An annotation @LogResult(key = "payTransition") and an aspect intercept the listener method, automatically writing 1 on success or 0 on exception, removing duplicated try/catch code.
6.4 Refactored SendEvent Method
The sendEvent method now accepts a key parameter, reads the flag from the extended state, persists only on success, and returns false when the business logic fails.
7. Conclusion
The article demonstrates a complete, production‑ready implementation of an order workflow using Spring Statemachine, covering state definition, configuration, persistence strategies, transactional listeners, testing, and advanced error‑handling techniques.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.