newcohospitality.com

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

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

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

    }

  3. 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);

    }

    }

  4. 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();

    }

    }

  5. 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());

    }

    }

  6. 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();

    }

    }

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