Effective Implementation of FSM in Spring Boot Using Spring State Machine
Written on
Finite State Machines (FSM) serve as a robust framework in programming, particularly beneficial for managing an object's or system's state in a structured manner. They enable the modeling of system behavior in reaction to varying inputs and states.
In the realm of Java development, FSMs prove invaluable for functions such as workflow oversight, automation of processes, and event-driven designs.
This article will guide you through the process of integrating FSM within a Spring Boot application by utilizing the Spring State Machine library, an efficient tool for overseeing state transitions in Java applications.
What Exactly is a Finite State Machine (FSM)?
An FSM is a computational framework designed for algorithm development. It is composed of a limited number of states, transitions among those states, and actions triggered upon entering or exiting a state.
At any given moment, the machine can occupy a single state, shifting from one to another in response to specific inputs.
In a Spring Boot application, FSMs can model various workflows, including order management in e-commerce platforms, document approval processes, or even user account lifecycles.
Setting Up Spring State Machine in Your Spring Boot Project
Add Dependencies
Begin by incorporating the Spring State Machine dependency into your build.gradle file:
implementation 'org.springframework.statemachine:spring-statemachine-core:3.2.1'
This will pull in the essential libraries needed to work with FSM in your Spring Boot application.
Define Your States and Events
Next, establish the states and events that your FSM will manage.
For instance, in an order processing scenario, an order could be in one of several states: PENDING, PROCESSING, SHIPPED, DELIVERED, or CANCELLED.
The events triggering transitions between these states may include actions like PROCESS_ORDER, SHIP_ORDER, or DELIVER_ORDER.
Here’s an example of how to define these states and events in code:
public enum OrderStates {
PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}
public enum OrderEvents {
PROCESS_ORDER, SHIP_ORDER, DELIVER_ORDER, CANCEL_ORDER
}
Configure the State Machine
Now, configure the state machine with the defined states and transitions.
This is usually accomplished in a class annotated with @Configuration. Here’s an example:
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
@Configuration
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.PENDING)
.state(OrderStates.PROCESSING)
.state(OrderStates.SHIPPED)
.state(OrderStates.DELIVERED)
.end(OrderStates.CANCELLED);
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStates.PENDING).target(OrderStates.PROCESSING).event(OrderEvents.PROCESS_ORDER)
.and()
.withExternal()
.source(OrderStates.PROCESSING).target(OrderStates.SHIPPED).event(OrderEvents.SHIP_ORDER)
.and()
.withExternal()
.source(OrderStates.SHIPPED).target(OrderStates.DELIVERED).event(OrderEvents.DELIVER_ORDER)
.and()
.withExternal()
.source(OrderStates.PENDING).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL_ORDER)
.and()
.withExternal()
.source(OrderStates.PROCESSING).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL_ORDER);
}
}
Triggering State Transitions
With the FSM configured, you can now initiate state transitions within your service layer.
Here’s an example of how to utilize the state machine in a service class:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.state.State;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class OrderService {
@Autowired
private StateMachine<OrderStates, OrderEvents> stateMachine;
public State<OrderStates, OrderEvents> getOrderStatus() {
stateMachine.startReactively().subscribe();
return stateMachine.getState();
}
public void processOrder() {
stateMachine.startReactively().subscribe();
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.PROCESS_ORDER).build()))
.subscribe();
}
public void shipOrder() {
stateMachine.startReactively().subscribe();
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.SHIP_ORDER).build()))
.subscribe();
}
public void deliverOrder() {
stateMachine.startReactively().subscribe();
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.DELIVER_ORDER).build()))
.subscribe();
}
public void cancelOrder() {
stateMachine.startReactively().subscribe();
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.CANCEL_ORDER).build()))
.subscribe();
}
}
Monitoring and Debugging the State Machine
Spring State Machine offers various tools for tracking and debugging state transitions.
You can attach listeners to your state machine to log state changes, making it easier to monitor the FSM’s operations.
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;
import org.springframework.stereotype.Component;
@Component
public class StateMachineListener extends StateMachineListenerAdapter<OrderStates, OrderEvents> {
@Override
public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) {
System.out.println("State changed from " + from.getId() + " to " + to.getId());
}
}
Testing the State Machine
It’s essential to thoroughly test your state machine to ensure it functions correctly.
You can create unit tests to verify that transitions occur as expected based on the events triggered.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.statemachine.StateMachine;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@SpringBootTest
class OrderServiceTest {
@Autowired
private StateMachine<OrderStates, OrderEvents> stateMachine;
@Test
void testStateMachineTransitions() {
StepVerifier.create(stateMachine.startReactively())
.expectComplete()
.verify();
// Send PROCESS_ORDER event and verify the state
StepVerifier.create(stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.PROCESS_ORDER).build())))
.expectNextMatches(result -> stateMachine.getState().getId() == OrderStates.PROCESSING)
.expectComplete()
.verify();
// Send SHIP_ORDER event and verify the state
StepVerifier.create(stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.SHIP_ORDER).build())))
.expectNextMatches(result -> stateMachine.getState().getId() == OrderStates.SHIPPED)
.expectComplete()
.verify();
// Send DELIVER_ORDER event and verify the state
StepVerifier.create(stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(OrderEvents.DELIVER_ORDER).build())))
.expectNextMatches(result -> stateMachine.getState().getId() == OrderStates.DELIVERED)
.expectComplete()
.verify();
}
}
Testing via `RestController`
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
public class TestController {
@Autowired
private OrderService orderService;
@GetMapping("/")
public String test(@RequestParam(name = "action", defaultValue = "") String action) {
if (Objects.equals(action, "process")) {
orderService.processOrder();
}
if (Objects.equals(action, "ship")) {
orderService.shipOrder();
}
if (Objects.equals(action, "deliver")) {
orderService.deliverOrder();
}
if (Objects.equals(action, "cancel")) {
orderService.cancelOrder();
}
return orderService.getOrderStatus().getId().toString();
}
}
You can now navigate to your browser at: http://127.0.0.1:9888/?action=deliver to test the outcome. If you attempt to access deliver directly, you will not receive the state DELIVERED initially, as you must first transition through PROCESSING, then SHIPPED, before reaching DELIVERED. The transitions must occur in sequence.
Implementing FSM within a Spring Boot application through Spring State Machine enables clear, maintainable, and scalable state management.
Whether dealing with simple tasks or intricate workflows, FSM can assist in handling state transitions systematically, enhancing both the reliability and clarity of your application.
By following the guidelines presented in this article, you can begin to integrate FSM into your Spring Boot projects and harness its robust capabilities.
In future discussions, we can delve into how to persist state within a data storage solution.
Happy coding! Thank you for reading.
If you find this information valuable, feel free to connect with me on LinkedIn | Medium.
Stackademic
Thank you for sticking with this article until the end. Before you leave:
- Please consider clapping and following the author!
- Follow us on X | LinkedIn | YouTube | Discord
- Explore our other platforms: In Plain English | CoFeed | Differ
- More content available at Stackademic.com