DEV Community

Solon Framework
Solon Framework

Posted on

Solon State Machine: When if-else Spaghetti Meets a Cleaner Pattern

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>
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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");
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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 -> { ... }));
Enter fullscreen mode Exit fullscreen mode

2. Inheritance style

public class MyMachine extends StateMachine<State, Event, Payload> {
    public MyMachine() {
        from(X).on(E).to(Y).then(ctx -> { ... });
    }
}
Enter fullscreen mode Exit fullscreen mode

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)