Understanding Finite State Machines and Implementing Spring Statemachine in Java

This article explains the fundamentals of finite state machines, illustrates their concepts and diagramming, and provides a comprehensive guide on using Spring Statemachine in Java—including schema design, configuration, event handling, persistence strategies, testing, and common pitfalls with solutions.

Top Architect
Top Architect
Top Architect
Understanding Finite State Machines and Implementing Spring Statemachine in Java

1. What Is a State Machine

A state (e.g., an automatic door can be open or closed) represents a distinct condition of a system. A finite state machine (FSM) models such conditions and the transitions between them, typically visualized as a state transition diagram.

1.1 State

Real‑world objects have discrete states; the FSM abstracts these into a mathematical model.

1.2 Four Core Concepts

State : the current condition (e.g., open, closed).

Event : a trigger that may cause a transition (e.g., pressing the open button).

Action : the operation performed when an event occurs (e.g., executing the open command).

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

1.3 Formal Definition

An FSM consists of a finite set of states, an initial state, inputs (events), and a transition function that determines the next state based on the current state and input.

2. State Machine Diagram Example

For an order processing scenario, the diagram includes:

Current State : e.g., WAIT_PAYMENT Condition/Event : e.g., PAYED Action : optional business logic after the event

Next State : e.g., WAIT_DELIVER Key pitfalls include treating actions as states and missing intermediate states, which can lead to incomplete transition logic.

3. Spring Statemachine

3.1 Overview

Spring Statemachine provides a framework for applying state‑machine concepts within Spring applications. It offers simple flat state machines, hierarchical structures, region support, guards, actions, and persistence adapters (in‑memory or Redis).

3.2 Quick Start

Database schema for an order table:

CREATE TABLE `tb_order` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_code` varchar(128) DEFAULT NULL COMMENT '订单编码',
  `status` smallint(3) DEFAULT NULL COMMENT '订单状态',
  `name` varchar(64) 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) DEFAULT NULL COMMENT '创建人',
  `update_user_code` varchar(32) DEFAULT NULL COMMENT '更新人',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
  `remark` varchar(64) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='订单表';

Dependency declarations (Maven):

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

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

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

3.5 Persistence Configuration

In‑memory persistence bean:

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

Redis persistence bean (requires Spring Redis configuration):

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

3.6 Business Layer

Controller example (REST endpoints for order operations):

@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 implementation uses the state machine to send events and persist state:

@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 pay(Long id) {
        Order order = orderMapper.selectById(id);
        if (!sendEvent(OrderStatusChangeEvent.PAYED, order)) {
            throw new RuntimeException("支付失败, 订单状态异常");
        }
        return order;
    }
    private synchronized boolean sendEvent(OrderStatusChangeEvent event, Order order) {
        boolean result = false;
        try {
            orderStateMachine.start();
            stateMachineMemPersister.restore(orderStateMachine, String.valueOf(order.getId()));
            Message<OrderStatusChangeEvent> message = MessageBuilder.withPayload(event)
                .setHeader("order", order).build();
            result = orderStateMachine.sendEvent(message);
            if (result) {
                stateMachineMemPersister.persist(orderStateMachine, String.valueOf(order.getId()));
            }
        } catch (Exception e) {
            // log error
        } finally {
            orderStateMachine.stop();
        }
        return result;
    }
}

3.7 Listener for State Changes

@Component("orderStateListener")
@WithStateMachine(name = "orderStateMachine")
public class OrderStateListenerImpl {
    @Resource
    private OrderMapper orderMapper;
    @OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")
    @Transactional(rollbackFor = Exception.class)
    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 exception
    }
    // other transition methods omitted for brevity
}

3.8 Common Issues and Solutions

Problem: orderStateMachine.sendEvent(message) always returns true, swallowing exceptions from listeners. Solution: store execution result in the state machine’s extended state (a map) and check it after sending the event. An AOP aspect can automatically record success (1) or failure (0) for each transition.

3.9 AOP Aspect for Result Recording

@Component
@Aspect
public class LogResultAspect {
    @Pointcut("@annotation(com.example.LogResult)")
    private void logResultPointCut() {}
    @Resource
    private StateMachine<OrderStatus, OrderStatusChangeEvent> orderStateMachine;
    @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;
        }
    }
}

Listeners are annotated with @LogResult(key = CommonConstants.payTransition) to automatically record the outcome.

4. Testing the Workflow

Typical API calls:

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

GET /order/pay?id=2 – trigger payment transition.

GET /order/deliver?id=2 – trigger delivery transition.

GET /order/receive?id=2 – trigger receipt transition.

Repeated payment attempts correctly raise an exception because the state machine prevents invalid transitions.

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.

BackendJavaworkflowstate machinePersistence
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.