skip to content

What are guards and actions in Spring Statemachine, and how do they differ?

level: middleimportance: must knowfreq 42%

answer

  1. Guard = boolean predicate, vetoes transition
  2. Action = side effect, void execute
  3. guardExpression = SpEL
  4. Order: guard -> exit -> transition action -> entry
  5. StateContext: event, headers, extended state

basics

~20 s

A guard is a boolean precondition on a transition — it decides whether the transition is allowed; returning false vetoes it. An action is code with side effects that runs when a transition is taken (or on state entry/exit).

solid answer

~40 s

Guards and actions are the two behavioural hooks. A **guard** implements Guard<S,E> (functional: boolean evaluate(StateContext)) and gates a transition: it's evaluated before the transition fires, and returning false blocks it so the event isn't accepted. You can also use guardExpression("...") with SpEL. Guards should be pure/side-effect-free predicates. An **action** implements Action<S,E> (void execute(StateContext)) and performs side effects — calling services, mutating extended state, publishing messages. Actions attach to transitions (.action(...)) and run during the transition, or to states as entry/exit/do actions. The order on a fired transition is: guard evaluated -> exit action of source (external) -> transition action -> entry action of target. Both receive a StateContext giving access to the event, its message headers, extended state variables, and the machine itself.

code

java · 21 lines
java
// Guard: only allow SHIP if payment cleared (data read from extended state)
Guard<OrderState, OrderEvent> paymentCleared =
    ctx -> Boolean.TRUE.equals(ctx.getExtendedState().get("paid", Boolean.class));

// Action: side effect + write to extended state, reading a header off the event
Action<OrderState, OrderEvent> reserveStock = ctx -> {
    Long orderId = ctx.getMessageHeaders().get("orderId", Long.class);
    warehouse.reserve(orderId);
    ctx.getExtendedState().getVariables().put("reserved", true);
};

// Wiring in configure(StateMachineTransitionConfigurer):
t.withExternal()
    .source(OrderState.PAID).target(OrderState.SHIPPED).event(OrderEvent.SHIP)
    .guard(paymentCleared)
    .action(reserveStock);

// Sending an event with data for the action to read:
stateMachine.sendEvent(Mono.just(
    MessageBuilder.withPayload(OrderEvent.SHIP).setHeader("orderId", 42L).build()
)).blockLast();

go deeper

for a junior

Knows guard = allow/deny, action = do something on transition.

for a middle

Can implement Guard<S,E>/Action<S,E>, use guardExpression SpEL, and read StateContext; knows the exit/transition/entry ordering.

for a senior

Insists guards are pure, handles action exceptions with error actions, uses entry/exit/do actions and passes data via Message headers.

for a principal

Designs the guard/action boundary for testability and idempotency, keeps external I/O robust, and avoids blocking the event loop with heavy entry actions.

## The two hooks Guards and actions let you attach logic to the state graph without polluting the graph itself. ### Guards — the gatekeepers A **guard** answers a yes/no question: *should this transition be allowed right now?* It implements the functional interface **`Guard<S, E>`**: ```java boolean evaluate(StateContext<S, E> context); ``` Returning **true** lets the transition proceed; **false** vetoes it — the event is then *not accepted* and the machine stays put (unless another transition for the same event/source has a passing guard). Guards are evaluated **before** the transition commits and before its action runs. Two ways to attach: - `.guard(myGuardBean)` — a `Guard<S,E>` implementation/lambda. - `.guardExpression("extendedState.variables.get('amount') != null")` — a **SpEL** expression evaluated against the `StateContext` (root object). Handy for simple checks without a class. **Golden rule:** guards must be **pure predicates** — no side effects. They can be evaluated and the transition may still not fire (e.g. another guard, or a choice pseudostate evaluating multiple guards). A guard that mutates state or calls external systems produces surprising, hard-to-test behaviour. ### Actions — the doers An **action** implements **`Action<S, E>`**: ```java void execute(StateContext<S, E> context); ``` Actions perform **side effects**: invoke a service, write to extended state, send a notification, publish an event. Actions attach in several places: - **Transition action** — `.action(myAction)`; runs when that transition fires. - **State entry action** — runs every time the state is entered. - **State exit action** — runs every time the state is left. - **State 'do' action** — runs while *in* the state (can be long-running/cancellable). - **Initial action** — runs once when the machine starts into the initial state. There's also an **error action** variant: `.action(action, errorAction)` on a transition, where the second runs if the primary throws. ## Ordering during a fired external transition 1. Guard(s) evaluated — if false, stop (event not accepted). 2. **Exit action** of the source state. 3. **Transition action(s)**. 4. **Entry action** of the target state. (An *internal* transition skips exit/entry — only the transition action runs, and the state doesn't change.) ## The StateContext — the shared toolbox Both guards and actions receive a **`StateContext<S,E>`**, the single most useful object. From it you get: the triggering **event** (`getEvent()`), the **Message** and its **headers** (`getMessageHeader(...)` — how you pass data along with an event), the **extended state** (`getExtendedState().getVariables()` — read/write shared data), the source/target states, and the `StateMachine` itself. This is how an action reads a payload passed on `sendEvent` and stashes a result for later guards/actions. ## Gotchas - **Side effects in guards** — non-deterministic, may run without the transition committing. - **Exceptions in actions** — by default can put the machine into an error state / surface via listeners; use the error-action overload or defensive coding for external calls. - **Assuming actions run on rejected events** — if the guard fails, the transition action never runs. - **Heavy work in entry actions** blocks the (reactive) event processing; offload long tasks to a 'do' action or async. - **Reading event data** — you must send data via `Message` headers and read it from `StateContext`, not via ad-hoc fields.

  • Why should guards be free of side effects?
    Guards can be evaluated without the transition ultimately firing (e.g. a false result, or a choice pseudostate probing several guards). Side effects would then run spuriously and be hard to reason about or test. Keep them pure predicates; put side effects in actions.
  • In what order do the exit action, transition action, and entry action run on an external transition?
    Guard first; if it passes, source-state exit action, then the transition action, then target-state entry action. An internal transition runs only the transition action and skips exit/entry.
  • How does an action get data that was supplied when the event was sent?
    Send the event as a Message with headers (MessageBuilder.setHeader) and read them in the action via StateContext.getMessageHeaders()/getMessageHeader(). Shared results go through the extended state variables map.

saying these in an interview costs you the question

  • Putting service calls or state mutation inside a guard
  • Believing the transition action runs even when the guard fails
  • Passing event data via mutable fields instead of Message headers/extended state
  • Doing long-running work in a state entry action and blocking event processing

context