Managing States with Spring State Machine
In the business logic of applications, there are some transitions from one state to another. For example, statuses of booking reservations or payment orders.
The straightforward way is to store statuses in a database and change it based on calls. But we may want to add additional checks to be sure that the transitions between statuses are always correct, especially when they become more complicated. State machine helps to handle this mess.
Spring State Machine
The Spring State Machine is an object in the heap that has some current state and controls transitions. Each object of business logic (e.g., booking #15623) must be associated with its own state machine object.
The configuration file specifies the initial, intermediate, and final, as well as the transitions between them. After that, you can be sure that the state will always be consistent.
Each transition has a start status, an end status, and an event on which the transition occurs. In addition, guards and actions can be added to transitions.
State persistence
There are 2 ways to store a state.
You can use additional libraries such as Spring Statemachine Data Jpa to persist all state machines, their contexts, states' history, transitions, as well as guards and actions. Each entity will be stored in the corresponding database table.
If your project is already big enough, it is unlikely you want to refactor it significantly. Therefore, the second option is to keep persisting states as it is, but on each transition:
- create an instance of the state machine
- load the initial state
- do the transition
- update the state in the database
Synchronous transactions
Spring State Machine version 3.0+ is fully reactive.
If the project is not based on the reactive stack, it makes sense to pay attention to version 2.5+, which is synchronous.
sendEvent
method of the synchronous version returns true
or false
. But true
is returned if the event is accepted, i.e. doesn’t violate the transition rules of the state machine. In this case, if an error occurred in the guard, action, or interceptor, then the method will still return true
.
This is rather inconvenient if you want to include state transition in the current database transaction. To solve this problem, you can write wrappers that put the error into the state machine context.
So this new method will throw an exception if there is an error in the context or the transition rules are violated.
This is basically it. The state machine allows you to keep all the transition rules in one config file, and be sure that the states of the entities are always consistent.