Backend Development 27 min read

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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Practical Guide to Spring StateMachine: Concepts, Implementation, and Persistence

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.

backendjavaaopspringPersistenceStateMachine
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

login 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.