Spring Flow State Machine
Goal
This project was born when we understood that spring-statemachine doesn’t meet our requirements. Our requirements are very simple:
-
It should be easy to use
-
It should be easy to integrate
-
It should manage not the state of our application, but the state of entities, which our application works with
-
It should be easy to configure and extend it
Actually, spring-statemachine doesn’t meet any of requirements.
Almost every system we work with has some entities which have its own lifecycle.
So we’ve created our own library, which allows you to achieve the following goals:
-
Define your own states for any entity
-
Define allowed transitions between states
-
Define actions, which should be performed before and after specific transition
-
Define actions, which should be performed before/after any transition
Non-goals
It is a non-goal to replace spring-statemachine (and we didn’t)
It is a non-goal to provide connectors to any available storage provider
Components
-
core
module provides all needed functionality for you to use spring-flow-statemachine -
starter
modules is Spring Boot Starter, which will configure as much as possible in your application for you.
Installation
Spring Boot
If you use Spring Boot then you can set up dependency on starter package
Maven
<dependency>
<groupId>ru.sberned.statemachine</groupId>
<artifactId>spring-flow-state-machine-starter</artifactId>
<version>1.1.1</version>
</dependency>
Gradle
compile 'ru.sberned.statemachine:spring-flow-state-machine-starter:1.1.1'
Spring without Boot
Otherwise you can set up dependency on state-machine-core artefact
How To
Configure
Step 1: Define state changer
StateChanger
is interface, which implementations are responsible for moving entity from one state to another (i.e. it can just update one field in your relational DB)
Step 2: Define item provider
ItemWithStateProvider
is interface which is responsible for finding object in store
Step 3: Define lock provider
It’s always possible in distributed systems that several nodes of your system will want to change the state of your entity simultaneously. We’re prepared for this case and ask you to provide us with Lock
which will guarantee, that only one transition can happen at a time. If you work in single-node environment — you can just return simple in-memory, otherwise you’ll need something distributed.
Interface, which should be implemented for this aim is LockProvider
. By default we provide you with very simple lock implementation, MapLockProvider
Step 4: Create and configure state machine
@Bean
public StateMachine<SimpleItem, SimpleState, String> stateMachine() {
StateRepository<SimpleItem, SimpleState, String> repository = StateRepositoryBuilder.<SimpleItem, SimpleState, String>configure() // ①
.setAvailableStates(EnumSet.allOf(SimpleState.class)) // ②
.setUnhandledMessageProcessor((item, state, type, ex) -> LOGGER.error("Got unhandled item with id {}, issue is {}", item, type)) // ③
.setAnyBefore((BeforeAnyTransition<SimpleItem, SimpleState>) (item, state) -> {
LOGGER.info("Started working on item with id {}", item.getId());
return true;
}) // ④
.defineTransitions()
.from(STARTED) // ⑤
.to(IN_PROGRESS) // ⑥
.after((AfterTransition<SimpleItem>) item -> LOGGER.info("Moved from STARTED to IN_PROGRESS"))
.and() // ⑦
// omitted for brevity
.build();
StateMachine<SimpleItem, SimpleState, String> stateMachine = new StateMachine<>(stateProvider(), stateChanger(), lockProvider); // ⑧
stateMachine.setStateRepository(repository); // ⑨
return stateMachine;
}
① — Everything starts with strongly-typed StateRepository#configure
method, where we define
-
Entity class
-
State class
-
Key class (it should be possible to fetch item with its state from your store by key of this type)
② — We think that it should be possible to use not all of the available states (i.e. if your application is in early stages of development), so you should pass subset of allowed states into method setAvailableStates
③ — You should, but not necessarily, provide an implementation of UnhandledMessageProcessor
. It’s always possible in distributed system that something will go wrong and we give you the ability to handle this.
④ — You can define several types of handlers for your state machine:
-
anyBefore
handlers will be executed before any transition -
before
handlers will be executed before concrete transition -
after
handlers will be executed after the concrete transition -
anyAfter
handlers will be executed after any transition
⑤ — from
should be read as "Transition may start at any of these states"
⑥ — to
should be read as "and can stop at any of these ones"
⑦ — and
is delimiter method between defining several transition rulesets
⑧ — Create StateMachine
itself
⑨ — Configure state machine behavior rules by providing it with StateRepository
Use
You have 2 ways to interact with state machine
Inject StateMachine
If you choose to inject StateMachine into your service, then you can call changeState
method. It returns map of your entity id to Future
of results of execution
Use event publisher
You can inject ApplicationEventPublisher
into your service and send `StateChangedEvent`s there. It is the type of one-way communication when you actually don’t care about the final result.
On-the fly state loading
Since 1.1.0
You can load states and transitions on the fly in runtime, but you need to think about several things:
-
All states should have correctly defined
equals
andhashCode
methods -
If you update state repository at runtime — it’s your responsibility to make all items not to have removed state as current (if any). But it’s considered to be a bad practice to remove states at runtime.
-
If you update state repository in your state machine then no guarantees is made for currently queued transitions. It’s entirely possible that some of them will fail because of new rules.
You can watch an example of dynamically loading and changing repository in samples
Requirements
Project requires Java 8 and Spring 4+
Tests and readiness
We’ve done our best to write as many tests as we can. Also, we use this project at work, so we think that this project is production-ready
Examples
You can find examples of usage in state-machine-samples module
Release notes
-
1.1.1: Fixes multithreading bug. Raises pitest coverage of core classes to 100%
-
1.1.0: Adds ability to use non-enum states
-
1.0.2: Initial release