Implementing a Finite State Machine for Order Processing with Spring StateMachine
This article explains the fundamentals of finite state machines, demonstrates how to model order lifecycle states and transitions in Java using Spring StateMachine, and provides detailed code examples for persistence, event handling, and advanced error‑handling techniques such as AOP and Redis storage.
State machines (finite‑state machines, FSM) are mathematical models that describe a set of states, events, actions, and transitions, allowing the behavior of real‑world objects—such as an automatic door—to be abstracted into a diagram.
The article introduces the four core concepts of a state machine: State (e.g., open , closed ), Event (trigger, e.g., button press), Action (operation performed when an event occurs), and Transition (state change from one state to another).
It then defines a finite‑state machine in Java using the Spring StateMachine framework, showing how to model an order processing workflow with the states WAIT_PAYMENT , WAIT_DELIVER , WAIT_RECEIVE , and FINISH , and the events PAYED , DELIVERY , and RECEIVED .
An example order table schema is provided:
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='订单表';Dependencies for Spring StateMachine and Redis persistence are added to the Maven pom:
<!-- redis persistence for state machine -->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-redis</artifactId>
<version>1.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>Enum definitions for order status and events are shown:
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;
}The state‑machine configuration registers the states and transitions:
@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 for in‑memory and Redis storage are defined, allowing the state machine to be restored across service restarts.
@Bean(name = "stateMachineMemPersister")
public static StateMachinePersister
getPersister() {
return new DefaultStateMachinePersister<>(new StateMachinePersist<>() {
private Map
map = new HashMap<>();
@Override
public void write(StateMachineContext context, String contextObj) {
map.put(contextObj, context);
}
@Override
public StateMachineContext read(String contextObj) {
return map.get(contextObj);
}
});
}
@Bean(name = "stateMachineRedisPersister")
public RedisStateMachinePersister
getRedisPersister(RedisConnectionFactory factory) {
RedisStateMachineContextRepository
repo = new RedisStateMachineContextRepository<>(factory);
RepositoryStateMachinePersist
persist = new RepositoryStateMachinePersist<>(repo);
return new RedisStateMachinePersister<>(persist);
}Controller and service layers expose REST endpoints for creating orders, paying, delivering, and confirming receipt. The service uses a synchronized sendEvent method to start the state machine, restore its previous state, send the event, and persist the new state only when the business logic succeeds.
@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"; }
// other endpoints omitted for brevity
}
@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, "payTransition")) { throw new RuntimeException("支付失败, 订单状态异常"); } return order; }
private synchronized boolean sendEvent(OrderStatusChangeEvent event, Order order, String key) {
boolean result = false;
try {
orderStateMachine.start();
stateMachineMemPersister.restore(orderStateMachine, String.valueOf(order.getId()));
Message
message = MessageBuilder.withPayload(event).setHeader("order", order).build();
result = orderStateMachine.sendEvent(message);
if (!result) return false;
Integer outcome = (Integer) orderStateMachine.getExtendedState().getVariables().get(key + order.getId());
orderStateMachine.getExtendedState().getVariables().remove(key + order.getId());
if (Objects.equals(1, outcome)) {
stateMachineMemPersister.persist(orderStateMachine, String.valueOf(order.getId()));
} else {
return false;
}
} catch (Exception e) {
// log error
} finally {
orderStateMachine.stop();
}
return result;
}
}Listeners annotated with @OnTransition update the order record and can throw exceptions to trigger transaction roll‑backs. To capture success/failure without losing the exception, the article introduces an AOP aspect that records a flag (1 for success, 0 for failure) in the state machine’s extended state.
@Aspect
@Component
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 annotation = method.getAnnotation(LogResult.class);
String key = annotation.key();
Object ret = null;
try {
ret = pjp.proceed();
orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 1);
} catch (Throwable e) {
orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 0);
throw e;
}
return ret;
}
}The article also discusses common pitfalls, such as the state machine swallowing exceptions and always returning true from sendEvent . By storing execution results in the extended state and checking them before persisting, developers can ensure that failed business logic does not corrupt the persisted workflow state.
Finally, the article provides a complete set of constants, example YAML configuration for Redis, and a summary of how to extend the approach to other transitions (delivery, receipt) using the same AOP‑based result‑logging mechanism.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
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.