Mastering Spring State Machine: Build Robust Order Workflows with Persistence

This article explains the fundamentals of finite state machines, introduces Spring Statemachine's features, and provides a step‑by‑step guide—including code samples, persistence options, testing, and AOP‑based error handling—to implement reliable order processing flows in Java backend applications.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Mastering Spring State Machine: Build Robust Order Workflows with Persistence

What is a State Machine

A state machine (finite‑state machine) is a mathematical model that describes a limited set of states and the transitions between them, such as an automatic door that can be open or closed .

Four Core Concepts

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

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

Action – the operation executed when an event occurs (e.g., the door actually opens).

Transition – the change from one state to another (e.g., open → closed ).

Spring Statemachine Overview

Spring Statemachine brings the state‑machine concept into Spring applications. It offers a flat single‑level state machine, hierarchical state configurations, regions for complex setups, guards, actions, a builder API, Zookeeper‑based distributed machines, event listeners, and easy persistence integration.

Quick Start

Below is a minimal example that models an order lifecycle (payment → delivery → receipt).

CREATE TABLE `tb_order` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_code` varchar(128) DEFAULT NULL COMMENT '订单编码',
  `status` smallint DEFAULT NULL COMMENT '订单状态',
  `name` varchar(64) DEFAULT NULL COMMENT '订单名称',
  `price` decimal(12,2) DEFAULT NULL COMMENT '价格',
  `delete_flag` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记',
  `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) DEFAULT NULL COMMENT '创建人',
  `update_user_code` varchar(32) DEFAULT NULL COMMENT '更新人',
  `version` int NOT NULL DEFAULT '0' COMMENT '版本号',
  `remark` varchar(64) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

Dependencies (Maven):

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

Define enums for states and events:

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

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

Persistence

Two persistence strategies are shown: in‑memory (Map) and Redis. The Redis persister is useful for distributed deployments.

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

Business Layer

Controller, service, and listener classes illustrate how to start the machine, send events, and update the order record. The listener uses @OnTransition methods to modify the database after each successful transition.

Testing the Flow

Typical REST calls:

POST /order/create – create a new order (initial state WAIT_PAYMENT).

GET /order/pay?id=2 – trigger PAYED event, moving to WAIT_DELIVER.

GET /order/deliver?id=2 – trigger DELIVERY event, moving to WAIT_RECEIVE.

GET /order/receive?id=2 – trigger RECEIVED event, moving to FINISH.

If an event is sent twice (e.g., paying an already paid order) the state machine returns false and the service throws a runtime exception.

Common Pitfall: Exceptions Are Swallowed

The state machine’s sendEvent method always returns true even when a listener throws an exception, making error detection difficult. The article proposes two solutions:

Store execution results in the machine’s ExtendedState variables (1 for success, 0 for failure).

Wrap listener methods with an AOP aspect that records the outcome in ExtendedState using a custom @LogResult annotation.

Example of the aspect:

@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) {
    orderStateMachine.getExtendedState().getVariables().put(key + order.getId(), 0);
    throw e;
  }
}

Final Thoughts

By leveraging ExtendedState and a lightweight AOP interceptor, developers can reliably detect listener failures, decide whether to persist the new state, and keep the order workflow consistent across memory and Redis persistence layers.

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.

JavaaopspringPersistenceStatemachine
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.