Fundamentals 26 min read

Mastering Finite State Machines with Spring Statemachine: A Complete Guide

Learn the fundamentals of finite state machines, explore their core concepts and four key elements, and see how to implement and persist a Spring Statemachine for order processing with detailed code examples, diagrams, and troubleshooting tips, while also understanding common pitfalls and best practices.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Mastering Finite State Machines with Spring Statemachine: A Complete Guide

What is a State Machine

A state (State) represents a specific condition of a real‑world entity. For example, an automatic door has two states: open and closed . A finite state machine (FSM) abstracts such behavior into a mathematical model consisting of a limited set of states and transitions.

State machine diagram
State machine diagram

Four Core Concepts

State – the condition of the system (e.g., open, closed).

Event – the trigger that causes a transition (e.g., press open button).

Action – the operation performed when an event occurs (e.g., open the door).

Transition – the change from one state to another (e.g., opening process).

Finite State Machine (FSM)

An FSM is defined by a set of states, an initial state, input events, and a transition function that maps a (current state, event) pair to the next state. It describes the lifecycle of an object and how it reacts to external events.

State Machine Diagram Example

Consider an order lifecycle: WAIT_PAYMENT → WAIT_DELIVER → WAIT_RECEIVE → FINISH . The diagram below illustrates the elements of the transition.

Order state machine diagram
Order state machine diagram

Spring Statemachine

Spring Statemachine provides a framework for using state‑machine concepts within Spring applications. It offers a flat single‑level machine, hierarchical configurations, region support, guards, actions, persistence adapters, and integration with Spring IoC.

Quick Start

Database schema for orders:

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='订单表';

1) Add Dependencies

<!-- redis persistence dependency -->
<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>

2) Define Enums

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; }

3) Configure the State Machine

@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderStatusChangeEvent> {
    @Override
    public void configure(StateMachineStateConfigurer<OrderStatus, OrderStatusChangeEvent> states) throws Exception {
        states.withStates()
              .initial(OrderStatus.WAIT_PAYMENT)
              .states(EnumSet.allOf(OrderStatus.class));
    }
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderStatusChangeEvent> 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);
    }
}

4) Persistence

In‑memory persistence using StateMachinePersister and Redis persistence via RedisStateMachinePersister. Configuration snippets are omitted for brevity.

5) Business Layer

Controller exposing REST endpoints for order creation, payment, delivery, and receipt.

@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("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    @Resource private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
    @Resource private StateMachinePersister<OrderStatus, OrderStatusChangeEvent, String> 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 are similar
    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 (Integer.valueOf(1).equals(flag)) {
                stateMachineMemPersister.persist(orderStateMachine, String.valueOf(order.getId()));
            } else {
                return false;
            }
        } catch (Exception e) {
            log.error("订单操作失败:{}", e);
            return false;
        } finally {
            orderStateMachine.stop();
        }
        return result;
    }
}

Testing the Workflow

Create an order: POST /order/create Pay the order: GET /order/pay?id=2 Deliver the order: GET /order/deliver?id=2 Confirm receipt:

GET /order/receive?id=2

Common Issues

The state machine may swallow exceptions thrown in transition listeners, causing sendEvent to always return true. This makes error detection difficult.

Exception handling screenshot
Exception handling screenshot

Solution: store the execution result in the state machine’s ExtendedState variables (e.g., 1 for success, 0 for failure) and check the flag before persisting.

ExtendedState Usage

orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 1); // success
// In catch block
orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 0); // failure

AOP Logging of Results

Define a custom annotation and aspect to automatically record success/failure without duplicating code.

@Retention(RetentionPolicy.RUNTIME)
public @interface LogResult { String key(); }
@Aspect
@Component
public class LogResultAspect {
    @Resource private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
    @Around("@annotation(logResult)")
    public Object logResultAround(ProceedingJoinPoint pjp, LogResult logResult) throws Throwable {
        Object[] args = pjp.getArgs();
        Message<?> message = (Message<?>) args[0];
        Order order = (Order) message.getHeaders().get("order");
        try {
            Object ret = pjp.proceed();
            orderStateMachine.getExtendedState().getVariables().put(logResult.key() + order.getId(), 1);
            return ret;
        } catch (Throwable e) {
            orderStateMachine.getExtendedState().getVariables().put(logResult.key() + order.getId(), 0);
            throw e;
        }
    }
}

Refactored Listener with AOP

@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<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        log.info("支付,状态机反馈信息:{}", message.getHeaders());
        order.setStatus(OrderStatus.WAIT_DELIVER.getKey());
        orderMapper.updateById(order);
        if (Objects.equals(order.getName(), "A")) { throw new RuntimeException("执行业务异常"); }
    }
    // deliverTransition and receiveTransition are similarly annotated
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaaopworkflowspringstate machinePersistence
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

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.