Practical Guide to Spring StateMachine: Concepts, Implementation, and Persistence
This article introduces the fundamentals of finite‑state machines, explains the four core concepts of state, event, action and transition, and provides a step‑by‑step Spring Statemachine implementation with database schema, enum definitions, configuration, persistence options, testing, common pitfalls and an AOP‑based solution for reliable transaction handling.
The article begins with a brief introduction to the concept of a state machine, describing it as a mathematical model that represents a limited set of states and the transitions between them, illustrated with the example of an automatic door that can be either open or closed .
Four essential concepts are then defined:
State : the current condition of the object (e.g., open , closed ).
Event : the trigger that causes a transition (e.g., pressing the open button).
Action : the operation performed when an event occurs (e.g., executing the open method).
Transition : the change from one state to another (e.g., open → closed ).
A finite‑state machine (FSM) is formally defined as a set of states, an initial state, inputs, and a transition function that determines the next state based on the current state and input.
The article then moves to the practical side, presenting a state‑machine diagram for an order‑processing workflow, which includes six elements: start, end, current state, target state, action, and condition.
Spring Statemachine Overview
Spring Statemachine provides a framework for using state‑machine concepts inside Spring applications. Its main features include a simple flat state machine, hierarchical state machines, region support, guards, actions, persistence adapters, a generator mode, Zookeeper‑based distributed state machines, event listeners, UML modeling, and integration with Spring IoC.
Quick Start – Database Schema
CREATE TABLE `tb_order` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_code` varchar(128) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单编码',
`status` smallint(3) DEFAULT NULL COMMENT '订单状态',
`name` varchar(64) COLLATE utf8mb4_bin 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) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人',
`update_user_code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
`remark` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='订单表';Sample data insertion is also shown.
Enum Definitions
public enum OrderStatus {
WAIT_PAYMENT(1, "待支付"),
WAIT_DELIVER(2, "待发货"),
WAIT_RECEIVE(3, "待收货"),
FINISH(4, "已完成");
private Integer key;
private String desc;
OrderStatus(Integer key, String desc) { this.key = key; this.desc = desc; }
public Integer getKey() { return key; }
public String getDesc() { return desc; }
public static OrderStatus getByKey(Integer key) {
for (OrderStatus e : values()) {
if (e.getKey().equals(key)) return e;
}
throw new RuntimeException("enum not exists.");
}
}
public enum OrderStatusChangeEvent { PAYED, DELIVERY, RECEIVED; }Spring Statemachine Configuration
@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);
}
}Persistence Beans
@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) throws Exception {
log.info("Persisting state machine, context:{}, contextObj:{}", JSON.toJSONString(context), JSON.toJSONString(contextObj));
map.put(contextObj, context);
}
@Override
public StateMachineContext read(Object contextObj) throws Exception {
log.info("Reading state machine, contextObj:{}", JSON.toJSONString(contextObj));
StateMachineContext ctx = (StateMachineContext) map.get(contextObj);
log.info("Read result:{}", JSON.toJSONString(ctx));
return ctx;
}
});
}
@Bean(name = "stateMachineRedisPersister")
public RedisStateMachinePersister
getRedisPersister() {
RedisStateMachineContextRepository
repository = new RedisStateMachineContextRepository<>(redisConnectionFactory);
RepositoryStateMachinePersist
p = new RepositoryStateMachinePersist<>(repository);
return new RedisStateMachinePersister<>(p);
}Controller Layer
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource private OrderService orderService;
@RequestMapping("/getById")
public Order getById(@RequestParam("id") Long id) { return orderService.getById(id); }
@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
@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)) throw new RuntimeException("支付失败, 订单状态异常"); return order; }
public Order deliver(Long id) { Order order = orderMapper.selectById(id); if (!sendEvent(OrderStatusChangeEvent.DELIVERY, order)) throw new RuntimeException("发货失败, 订单状态异常"); return order; }
public Order receive(Long id) { Order order = orderMapper.selectById(id); if (!sendEvent(OrderStatusChangeEvent.RECEIVED, order)) throw new RuntimeException("收货失败, 订单状态异常"); return order; }
private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order) {
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) {
Integer flag = (Integer) orderStateMachine.getExtendedState().getVariables().get(CommonConstants.payTransition + order.getId());
orderStateMachine.getExtendedState().getVariables().remove(CommonConstants.payTransition + order.getId());
if (Objects.equals(1, flag)) {
stateMachineMemPersister.persist(orderStateMachine, String.valueOf(order.getId()));
} else {
return false;
}
}
} catch (Exception e) { log.error("订单操作失败:{}", e); }
finally { orderStateMachine.stop(); }
return result;
}
}State‑Machine Listener
@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");
log.info("支付,状态机反馈信息:{}", message.getHeaders().toString());
order.setStatus(OrderStatus.WAIT_DELIVER.getKey());
orderMapper.updateById(order);
if (Objects.equals(order.getName(), "A")) { throw new RuntimeException("执行业务异常"); }
}
@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
@LogResult(key = CommonConstants.deliverTransition)
public void deliverTransition(Message
message) {
Order order = (Order) message.getHeaders().get("order");
log.info("发货,状态机反馈信息:{}", message.getHeaders().toString());
order.setStatus(OrderStatus.WAIT_RECEIVE.getKey());
orderMapper.updateById(order);
}
@OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
@LogResult(key = CommonConstants.receiveTransition)
public void receiveTransition(Message
message) {
Order order = (Order) message.getHeaders().get("order");
log.info("确认收货,状态机反馈信息:{}", message.getHeaders().toString());
order.setStatus(OrderStatus.FINISH.getKey());
orderMapper.updateById(order);
}
}Problem: Exceptions are swallowed by the state machine
The article explains that orderStateMachine.sendEvent(message) always returns true even when the listener throws an exception, making it impossible to detect failures directly.
Solution: Use ExtendedState to store execution results
Listeners write a flag (1 for success, 0 for failure) into orderStateMachine.getExtendedState().getVariables() . The sending method reads this flag and decides whether to persist the state.
To avoid repetitive code, an AOP annotation @LogResult and an aspect are introduced. The aspect intercepts listener methods, captures the Message argument, and records the success flag in the extended state automatically.
AOP Annotation and Aspect
@Retention(RetentionPolicy.RUNTIME)
public @interface LogResult { String key(); }
@Aspect
@Component
@Slf4j
public class LogResultAspect {
@Resource private StateMachine
orderStateMachine;
@Pointcut("@annotation(com.example.LogResult)")
private void logResultPointCut() {}
@Around("logResultPointCut()")
public Object logResultAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Message message = (Message) args[0];
Order order = (Order) message.getHeaders().get("order");
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
LogResult logResult = method.getAnnotation(LogResult.class);
String key = logResult.key();
try {
Object ret = pjp.proceed();
orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 1);
return ret;
} catch (Throwable e) {
log.error("e:{}", e.getMessage());
orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 0);
throw e;
}
}
}With this setup, the service’s sendEvent method reads the flag using the same key and only persists the state when the flag equals 1 , otherwise it returns false to indicate failure.
The article concludes with a reminder to support the author and provides links to the full source code and a knowledge‑sharing community.
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
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.