From spring
Model explicit application lifecycles with Spring Statemachine states, events, guards, actions, extended state, persistence, and state-machine tests. Use this skill when modeling explicit application lifecycles with Spring Statemachine, including states, events, guards, actions, extended state, persistence, and state-machine tests.
npx claudepluginhub ririnto/sinon --plugin springThis skill uses the workspace's default tool permissions.
Use this skill when modeling explicit application lifecycles with Spring Statemachine, including states, events, guards, actions, extended state, persistence boundaries, and state-machine tests.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use this skill when modeling explicit application lifecycles with Spring Statemachine, including states, events, guards, actions, extended state, persistence boundaries, and state-machine tests.
The current stable Spring Statemachine line is 4.0.1. The official project README marks the project as maintenance mode, so prefer the ordinary, well-supported configuration path unless the workflow clearly needs factories, persistence, pseudo states, or reactive dispatch.
Use spring-statemachine for finite-state lifecycle modeling where legal transitions matter and invalid event handling must be explicit.
The ordinary Spring Statemachine job is:
| Situation | Stay here or open a reference |
|---|---|
| One singleton machine with external transitions, guards, actions, listeners, and in-memory tests | Stay in SKILL.md |
| Many machine instances, persisted state, regions, or persistence-aware tests | Open references/when-single-machine-lifecycle-is-not-enough.md |
| Choice, junction, fork, join, or history semantics | Open references/pseudo-states.md |
| Reactive guards, actions, or event flows | Open references/reactive-support.md |
| Situation | Use |
|---|---|
| Transition eligibility depends on machine data | guard |
| A transition must trigger a side effect | action |
| Small workflow context must travel with the machine | extended state |
| The application must observe lifecycle movement | StateMachineListener |
| One topology must create many runtime instances or survive restart | factory and persistence support |
Use the BOM once and keep the Statemachine modules versionless underneath it. When Spring Boot already manages the same Statemachine line, keep the child artifacts versionless there too and only pin the BOM or module version when the example is intentionally overriding the managed line.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-bom</artifactId>
<version>4.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Use the starter for ordinary Boot-based state machine work and the test module for state-machine tests.
<dependencies>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
./mvnw test -Dtest=OrderStateMachineTests
./gradlew test --tests OrderStateMachineTests
enum States { NEW, PAID, SHIPPED, CANCELLED }
enum Events { PAY, SHIP, CANCEL }
@Configuration
@EnableStateMachine
class OrderStateMachineConfig {
}
Start with one machine and one clear lifecycle. Add factories, persistence, pseudo states, or regions only when the workflow truly requires them.
enum States { NEW, PAID, SHIPPED, CANCELLED }
enum Events { PAY, SHIP, CANCEL }
@Configuration
@EnableStateMachine
class OrderStateMachineConfig extends StateMachineConfigurerAdapter<States, Events> {
@Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states.withStates()
.initial(States.NEW)
.state(States.PAID)
.end(States.SHIPPED)
.end(States.CANCELLED);
}
@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions
.withExternal().source(States.NEW).target(States.PAID).event(Events.PAY)
.and()
.withExternal().source(States.PAID).target(States.SHIPPED).event(Events.SHIP)
.and()
.withExternal().source(States.NEW).target(States.CANCELLED).event(Events.CANCEL);
}
}
@Bean
Guard<States, Events> paymentAllowed() {
return context -> Boolean.TRUE.equals(context.getExtendedState().get("paymentAllowed", Boolean.class));
}
@Bean
Action<States, Events> reserveInventory() {
return context -> {
Long orderId = context.getMessageHeaders().get("orderId", Long.class);
inventoryService.reserve(orderId);
};
}
stateMachine.getExtendedState().getVariables().put("paymentAllowed", true);
@Bean
StateMachineListener<States, Events> stateChangeListener() {
return new StateMachineListenerAdapter<>() {
@Override
public void stateChanged(State<States, Events> from, State<States, Events> to) {
auditService.record(from == null ? null : from.getId(), to == null ? null : to.getId());
}
};
}
@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception {
config.withConfiguration().listener(stateChangeListener());
}
boolean accepted = stateMachine.sendEvent(Events.PAY);
@SpringBootTest
class OrderStateMachineTests {
@Autowired
StateMachine<States, Events> stateMachine;
@Test
void payTransitionReachesPaidState() {
stateMachine.start();
boolean accepted = stateMachine.sendEvent(Events.PAY);
assertAll(
() -> assertTrue(accepted),
() -> assertEquals(States.PAID, stateMachine.getState().getId())
);
}
@Test
void invalidEventLeavesStateUnchanged() {
stateMachine.start();
stateMachine.sendEvent(Events.SHIP);
assertEquals(States.NEW, stateMachine.getState().getId());
}
}
Return:
assertAll(...) when one test evaluates multiple assertions for the same transition outcome.