Mastering Spring Statemachine: Build Robust Order Workflows with Persistent State

This guide explains the fundamentals of state machines, introduces the four core concepts, shows how to model order transitions with diagrams, and provides a complete Spring Statemachine implementation—including database schema, enums, configuration, persistence, controller, service, listeners, testing, and advanced error‑handling techniques—so developers can create reliable, persisted workflows in Java.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Mastering Spring Statemachine: Build Robust Order Workflows with Persistent State

What is a State Machine

First, let’s explain what a "state" is. Real‑world objects have different states; for example, an automatic door can be open or closed . A state machine (specifically a finite‑state machine) models a system that has a limited number of states, such as the two states of the door.

A state machine is a mathematical model, essentially a state‑transition diagram. For the automatic door, the diagram shows two states (open and closed) and the transitions triggered by events (e.g., receiving an open‑door signal when closed).

Given a state machine, its current state, and an input, the next state can be deterministically computed. This completes the basic definition of a state machine: a finite‑state automaton abstracting the operational rules of real‑world entities.

Four Major Concepts

State : The distinct conditions a machine can be in (e.g., open, closed).

Event : The trigger that causes a transition (e.g., pressing the open‑door button).

Action : The operation performed when an event occurs (e.g., the door actually opens).

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

Finite‑State Machine (FSM)

An FSM, also called a finite‑state automaton, is a mathematical model that represents a finite set of states, the transitions between them, and associated actions.

It consists of a set of states, an initial state, inputs, and a transition function that maps the current state and input to the next state. Its purpose is to describe the sequence of states an object goes through during its lifecycle and how it reacts to external events.

State Machine Diagram

When designing requirements, you need six elements to complete a state‑machine diagram: start, end, current state, next state (target), action, and condition.

Example – order processing: transition from Pending Payment to Pending Shipment .

Current state: Pending Payment

Condition (event): Payment received

Action: After the payment action, the state can transition to Pending Shipment (or remain unchanged if no condition is met).

Next state: Pending Shipment

Key points:

Do not treat a procedural action as a state; actions are transient, while states persist until an external condition triggers a change.

Missing states can cause incomplete transition logic; always review the state diagram or table to ensure a robust design.

Spring Statemachine

Overview

Spring Statemachine is a framework that brings state‑machine concepts into Spring applications.

Easy‑to‑use flat single‑level state machine for simple scenarios.

Hierarchical state‑machine structure to simplify complex configurations.

Region support for more complex state configurations.

Support for triggers, transitions, guards, and actions.

Type‑safe configuration adapters.

Builder pattern for simple instantiation outside the Spring context.

Redis‑based distributed state machine.

State‑machine event listeners.

UML modeling with Eclipse Papyrus.

Persist configuration to permanent storage.

Spring IoC integration to bind beans with the state machine.

The state machine is powerful because its behavior is consistent, making debugging easier. The idea is that an application exists in a finite set of states, and predefined triggers move it between states, based on events or timers.

Quick Start

Example order table DDL:

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

/* Data for the table `tb_order` */
INSERT INTO `tb_order`(`id`,`order_code`,`status`,`name`,`price`,`delete_flag`,`create_time`,`update_time`,`create_user_code`,`update_user_code`,`version`,`remark`) VALUES
(2,'A111',1,'A','22.00',0,'2022-10-15 16:14:11','2022-10-02 21:29:14','zhangsan','zhangsan',0,NULL),
(3,'A111',1,'订单A','22.00',0,'2022-10-02 21:53:13','2022-10-02 21:29:14','zhangsan','zhangsan',0,NULL),
(4,'A111',1,'订单A','22.00',0,'2022-10-02 21:53:13','2022-10-02 21:29:14','zhangsan','zhangsan',0,NULL),
(5,'A111',1,'订单A','22.00',0,'2022-10-03 09:08:30','2022-10-02 21:29:14','zhangsan','zhangsan',0,NULL);

Dependencies:

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

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

State‑machine configuration:

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

Persistence configuration (in‑memory and Redis):

@Configuration
public class Persist<E, S> {
    @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 {
                map.put(contextObj, context);
            }
            @Override
            public StateMachineContext read(Object contextObj) throws Exception {
                return (StateMachineContext) map.get(contextObj);
            }
        });
    }
    @Resource
    private RedisConnectionFactory redisConnectionFactory;
    @Bean(name = "stateMachineRedisPersister")
    public RedisStateMachinePersister<E, S> getRedisPersister() {
        RedisStateMachineContextRepository<E, S> repository = new RedisStateMachineContextRepository<>(redisConnectionFactory);
        RepositoryStateMachinePersist<E, S> p = new RepositoryStateMachinePersist<>(repository);
        return new RedisStateMachinePersister<>(p);
    }
}

Controller (REST endpoints):

@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 (core business logic and event sending):

@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;
    }
    public Order deliver(Long id) {
        Order order = orderMapper.selectById(id);
        if (!sendEvent(OrderStatusChangeEvent.DELIVERY, order, CommonConstants.deliverTransition)) {
            throw new RuntimeException("发货失败, 订单状态异常");
        }
        return order;
    }
    public Order receive(Long id) {
        Order order = orderMapper.selectById(id);
        if (!sendEvent(OrderStatusChangeEvent.RECEIVED, order, CommonConstants.receiveTransition)) {
            throw new RuntimeException("收货失败, 订单状态异常");
        }
        return order;
    }
    private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order, String key) {
        boolean result = false;
        try {
            orderStateMachine.start();
            stateMachineMemPersister.restore(orderStateMachine, String.valueOf(order.getId()));
            Message<OrderStatusChangeEvent> message = MessageBuilder.withPayload(changeEvent)
                .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;
    }
}

Listener that updates the order status and records execution results using the extended state:

@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");
        order.setStatus(OrderStatus.WAIT_DELIVER.getKey());
        orderMapper.updateById(order);
        // optional business logic, may throw RuntimeException to simulate failure
    }
    @OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")
    @LogResult(key = CommonConstants.deliverTransition)
    public void deliverTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.WAIT_RECEIVE.getKey());
        orderMapper.updateById(order);
    }
    @OnTransition(source = "WAIT_RECEIVE", target = "FINISH")
    @LogResult(key = CommonConstants.receiveTransition)
    public void receiveTransition(Message<OrderStatusChangeEvent> message) {
        Order order = (Order) message.getHeaders().get("order");
        order.setStatus(OrderStatus.FINISH.getKey());
        orderMapper.updateById(order);
    }
}

Testing the workflow:

Create an order: http://localhost:8084/order/create Pay the order: http://localhost:8084/order/pay?id=2 Deliver the order: http://localhost:8084/order/deliver?id=2 Confirm receipt: http://localhost:8084/order/receive?id=2 If an operation is repeated (e.g., paying an already‑paid order), the state machine throws an exception, which is captured and logged.

Problem: State Machine Swallows Exceptions

The orderStateMachine.sendEvent(message) call always returns

true</strong>, even when the listener throws an exception, making it impossible to detect failures directly.</p>
<p>Solution: Use the state machine’s <code>ExtendedState

to store execution results (1 for success, 0 for failure). The listener writes the result, and the sender reads it to decide whether to persist the state.

public interface ExtendedState {
    Map<Object, Object> getVariables();
    <T> T get(Object var1, Class<T> var2);
    void setExtendedStateChangeListener(ExtendedStateChangeListener listener);
    interface ExtendedStateChangeListener { void changed(Object var1, Object var2); }
}

public class DefaultExtendedState implements ExtendedState {
    private final Map<Object, Object> variables = new ObservableMap(new ConcurrentHashMap(), new LocalMapChangeListener());
    public Map<Object, Object> getVariables() { return this.variables; }
}

Listener now records the outcome:

@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
@Transactional
@LogResult(key = CommonConstants.payTransition)
public void payTransition(Message<OrderStatusChangeEvent> message) {
    Order order = (Order) message.getHeaders().get("order");
    // business logic … may throw RuntimeException
    // result is stored by @LogResult aspect
}

The sendEvent method reads the variable and persists only on success, otherwise returns false.

Optimization Notes

The current implementation only checks the payTransition result; similar handling is needed for delivery and receipt events.

Repeated code for setting the extended‑state variable can be extracted into an AOP aspect.

Constants used for extended‑state keys:

public interface CommonConstants {
    String orderHeader = "order";
    String payTransition = "payTransition";
    String deliverTransition = "deliverTransition";
    String receiveTransition = "receiveTransition";
}

Custom Annotation and Aspect for Result Logging

@Retention(RetentionPolicy.RUNTIME)
public @interface LogResult {
    /** Business key used in the extended state */
    String key();
}
@Component
@Aspect
@Slf4j
public class LogResultAspect {
    @Resource
    private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
    @Pointcut("@annotation(com.example.aop.annotation.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();
        Object returnVal = null;
        try {
            returnVal = pjp.proceed();
            orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 1);
        } catch (Throwable e) {
            log.error("e:{}", e.getMessage());
            orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 0);
            throw e;
        }
        return returnVal;
    }
}

Listeners are now annotated with @LogResult to automatically record success or failure without manual boilerplate.

Source: 猿码技术专栏
state machinePersistencespring statemachineFinite State Machine
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

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.