The Core Idea
A state machine has four building blocks:
| Concept | Meaning |
|---|---|
| State | A condition the system is in (e.g., "CREATED", "PAID") |
| Event | Something that happens (e.g., "PAY", "SHIP") |
| Action | What runs when a transition happens |
| Transition | The rule: from state X, on event Y, go to state Z (optionally if condition W, and do action A) |
Solon's StateMachine<State, Event, Payload> ties them together with a fluent DSL.
Hello World: A Light Switch
Add the dependency:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-statemachine</artifactId>
</dependency>
Now, a light switch that toggles between OFF and ON:
import org.noear.solon.statemachine.EventContext;
import org.noear.solon.statemachine.StateMachine;
public class LightSwitchApp {
enum Event { PRESS }
enum State { OFF, ON }
public static void main(String[] args) {
StateMachine<State, Event, String> stateMachine = new StateMachine<>();
// OFF + PRESS -> ON
stateMachine.addTransition(t -> t.from(State.OFF).on(Event.PRESS).to(State.ON)
.then(context -> System.out.println("Light is ON")));
// ON + PRESS -> OFF
stateMachine.addTransition(t -> t.from(State.ON).on(Event.PRESS).to(State.OFF)
.then(context -> System.out.println("Light is OFF")));
// Test it
State currentState = State.OFF;
for (int i = 0; i < 6; i++) {
currentState = stateMachine.sendEvent(
Event.PRESS,
EventContext.of(currentState, null)
);
System.out.println("New state: " + currentState);
}
}
}
Run it, and the light toggles back and forth. The sendEvent() returns the new state -- you don't manage a state variable manually; you just hand over the current state and let the machine compute the next.
The DSL Breakdown
The StateTransitionDecl has five methods:
| Method | Required | Description |
|---|---|---|
.from(state) |
Yes | Source state(s) -- can have multiple |
.to(state) |
Yes | Target state (one) |
.on(event) |
Yes | Triggering event |
.when(condition) |
No | Optional guard condition |
.then(action) |
No | Optional action to execute |
The when and then are optional. If you just need a simple state transition without side effects:
stateMachine.addTransition(t ->
t.from(State.DRAFT).to(State.PUBLISHED).on(Event.PUBLISH));
Real-World Example: Order State Machine
Let's model an order lifecycle with states NONE -> CREATED -> PAID -> SHIPPED -> DELIVERED, with optional cancellation paths.
public enum OrderEvent { CREATE, PAY, SHIP, DELIVER, CANCEL }
public enum OrderState { NONE, CREATED, PAID, SHIPPED, DELIVERED, CANCELLED }
Define an Order payload that implements EventContext:
public class Order implements EventContext<OrderState, Order> {
private String id;
private String product;
private OrderState state;
private String status;
@Override
public OrderState getState() { return state; }
@Override
public Order getPayload() { return this; }
public void setState(OrderState state) { this.state = state; }
public void setStatus(String status) { this.status = status; }
}
Now build the state machine by extending StateMachine:
public class OrderStateMachine extends StateMachine<OrderState, OrderEvent, Order> {
public OrderStateMachine() {
from(OrderState.NONE).on(OrderEvent.CREATE).to(OrderState.CREATED).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.CREATED);
payload.setStatus("Created");
});
from(OrderState.CREATED).on(OrderEvent.PAY).to(OrderState.PAID).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.PAID);
payload.setStatus("Paid");
});
from(OrderState.PAID).on(OrderEvent.SHIP).to(OrderState.SHIPPED).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.SHIPPED);
payload.setStatus("Shipped");
});
from(OrderState.SHIPPED).on(OrderEvent.DELIVER).to(OrderState.DELIVERED).then(ctx -> {
Order payload = ctx.getPayload();
payload.setState(OrderState.DELIVERED);
payload.setStatus("Delivered");
});
}
}
Test it:
public class OrderTest {
public static void main(String[] args) {
OrderStateMachine stateMachine = new OrderStateMachine();
Order order = new Order("1", "iPhone 16 Pro Max", null);
stateMachine.sendEvent(OrderEvent.CREATE, order);
stateMachine.sendEvent(OrderEvent.PAY, order);
stateMachine.sendEvent(OrderEvent.SHIP, EventContext.of(order.getState(), order));
stateMachine.sendEvent(OrderEvent.DELIVER, EventContext.of(order.getState(), order));
}
}
Two Usage Styles
1. Lambda style (composition)
StateMachine<State, Event, Payload> sm = new StateMachine<>();
sm.addTransition(t -> t.from(X).on(E).to(Y).then(ctx -> { ... }));
2. Inheritance style
public class MyMachine extends StateMachine<State, Event, Payload> {
public MyMachine() {
from(X).on(E).to(Y).then(ctx -> { ... });
}
}
Both produce the same result. Use composition when you need multiple independent machines; use inheritance when the machine is a self-contained domain concept.
When to Use It (and When Not To)
Solon State Machine is designed as a complement to Solon Flow, not a replacement.
Use State Machine when:
- You have discrete states and events (order lifecycle, document workflow, game state)
- You want a declarative alternative to nested if-else or switch statements
- You don't need persistence -- the state is maintained in memory via the event context
Use Solon Flow when:
- You need persistent, resumable long-running processes
- You need a visual designer for business users
- You need complex branching, parallel execution, or sub-processes
The two can coexist in the same project.
The Honest Picture
No persistence. The state machine is stateless by design -- you pass the current state in via EventContext, and the machine returns the new state. You own the storage.
No visual designer. Unlike Solon Flow which has a web-based designer, the state machine is code-only.
No built-in event bus integration. State transitions trigger .then() actions synchronously.
Simple but not powerful for complex workflows. If your state logic involves parallel states or nested state machines, Solon Flow is the better choice.
Summary
| Aspect | Details |
|---|---|
| Dependency |
org.noear:solon-statemachine (since v3.4.3) |
| Core class | StateMachine<State, Event, Payload> |
| DSL | .from().on().to().when().then() |
| Context |
EventContext.of(state, payload) or custom impl |
| Persistence | None (caller-owned, context-passed) |
| Best for | Order lifecycle, document workflow, game state, toggles |
All examples in this article are from the official documentation at solon.noear.org.
Top comments (0)