10. Statemachine Configuration

One of the common tasks when using a Statemachine is to design its runtime configuration. This chapter will focus on how Spring Statemachine is configured and how it leverages Spring’s lightweight IoC containers to simplify the application internals to make it more manageable.

[Note]Note

Configuration examples in this section are not feature complete, i.e. you always need to have definitions of both states and transitions, otherwise state machine configuration would be ill-formed. We have simply made code snippets less verbose by leaving other needed parts away.

10.1 Using enable annotations

We use familiar spring enabler annotations to ease configuration. Two annotations exists, @EnableStateMachine and @EnableStateMachineFactory. These annotations if placed in a @Configuration class will enable some basic functionality needed by a state machines.

@EnableStateMachine is used when a configuration wants to create an instance of a StateMachine. Usually @Configuration class extends adapters EnumStateMachineConfigurerAdapter or StateMachineConfigurerAdapter which allows user to override configuration callback methods. We automatically detect if user is using these adapter classes and modify runtime configuration logic.

@EnableStateMachineFactory is used when a configuration wants to create an instance of a StateMachineFactory.

[Note]Note

Usage examples of these are shown in below sections.

10.2 Configuring States

We’ll get into more complex configuration examples a bit later but let’s first start with a something simple. For most simple state machine you just use EnumStateMachineConfigurerAdapter and define possible states, choose initial and optional end state.

@Configuration
@EnableStateMachine
public class Config1Enums
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1)
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

}

It’s also possible to use strings instead of enums as states and events by using StateMachineConfigurerAdapter as shown below. Most of a configuration examples is using enums but generally speaking strings and enums can be just interchanged.

@Configuration
@EnableStateMachine
public class Config1Strings
        extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
            .withStates()
                .initial("S1")
                .end("SF")
                .states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4")));
    }

}
[Note]Note

Using enums will bring more safe set of states and event types but limits possible combinations to compile time. Strings don’t have this limitation and allows user to use more dynamic ways to build state machine configurations but doesn’t allow same level of safety.

10.3 Configuring Hierarchical States

Hierarchical states can be defined by using multiple withStates() calls where parent() can be used to indicate that these particular states are sub-states of some other state.

@Configuration
@EnableStateMachine
public class Config2
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1)
                .state(States.S1)
                .and()
                .withStates()
                    .parent(States.S1)
                    .initial(States.S2)
                    .state(States.S2);
    }

}

10.4 Configuring Regions

There are no special configuration methods to mark a collection of states to be part of an orthogonal state. To put it simple, orthogonal state is created when same hierarchical state machine has multiple set of states each having a initial state. Because an individual state machine can only have one initial state, multiple initial states must mean that a specific state must have multiple independent regions.

@Configuration
@EnableStateMachine
public class Config10
        extends EnumStateMachineConfigurerAdapter<States2, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States2, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States2.S1)
                .state(States2.S2)
                .and()
                .withStates()
                    .parent(States2.S2)
                    .initial(States2.S2I)
                    .state(States2.S21)
                    .end(States2.S2F)
                    .and()
                .withStates()
                    .parent(States2.S2)
                    .initial(States2.S3I)
                    .state(States2.S31)
                    .end(States2.S3F);
    }

}

10.5 Configuring Transitions

We support three different types of transitions, external, internal and local. Transitions are either triggered by a signal which is an event sent into a state machine or a timer.

@Configuration
@EnableStateMachine
public class Config3
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1)
                .states(EnumSet.allOf(States.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(States.S1).target(States.S2)
                .event(Events.E1)
                .and()
            .withInternal()
                .source(States.S2)
                .event(Events.E2)
                .and()
            .withLocal()
                .source(States.S2).target(States.S3)
                .event(Events.E3);
    }

}

10.6 Configuring Guards

Guards are used to protect state transitions. Interface Guard is used to do an evaluation where method has access to a StateContext.

@Configuration
@EnableStateMachine
public class Config4
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(States.S1).target(States.S2)
                .event(Events.E1)
                .guard(guard())
                .and()
            .withExternal()
                .source(States.S2).target(States.S3)
                .event(Events.E2)
                .guardExpression("true");

    }

    @Bean
    public Guard<States, Events> guard() {
        return new Guard<States, Events>() {

            @Override
            public boolean evaluate(StateContext<States, Events> context) {
                return true;
            }
        };
    }

}

In above two different types of guard configurations are used. Firstly a simple Guard is created as a bean and attached to transition between states S1 and S2.

Secondly a simple SPeL expression can be used as a guard where expression must return a BOOLEAN value. Behind a scenes this expression based guard is a SpelExpressionGuard. This was attached to transition between states S2 and S3. Both guard in above sample always evaluate to true.

10.7 Configuring Actions

Actions can be defined to be executed with transitions and states itself. Action is always executed as a result of a transition which originates from a trigger.

@Configuration
@EnableStateMachine
public class Config51
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(States.S1)
                .target(States.S2)
                .event(Events.E1)
                .action(action());
    }

    @Bean
    public Action<States, Events> action() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                // do something
            }
        };
    }

}

In above a single Action is defined as bean action and associated with a transition from S1 to S2.

@Configuration
@EnableStateMachine
public class Config52
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1, action())
                .state(States.S1, action(), null)
                .state(States.S2, null, action())
                .state(States.S2, action())
                .state(States.S3, action(), action());
    }

    @Bean
    public Action<States, Events> action() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                // do something
            }
        };
    }

}
[Note]Note

Usually you would not define same Action instance for different stages but we did it here not to make too much noise in a code snippet.

In above a single Action is defined as bean action and associated with states S1, S2 and S3. There is more going on there which needs more clarification:

  • We defined action for initial state S1.
  • We defined entry action for state S1 and left exit action empty.
  • We defined exit action for state S2 and left entry action empty.
  • We defined a single state action for state S2.
  • We defined entry action as well as exit action for state S3.
  • Notice how state S1 is used twice with initial() and state() functions. This is only needed if you want to define entry or exit actions with initial state.
[Important]Important

Defining action with initial() function only executes particular action when state machine or sub state is started. Think this action to be initializing action which is only executed once. Action defined with state() is then executed if state machine is transitioning back and forward between initial and non-initial states.

10.7.1 State Actions

State actions are executed differently compared to entry and exit actions simply because execution happens after state has been entered and can be cancelled if state exit happens before particular action has been completed.

State Actions are executed using a normal Spring TaskScheduler wrapped within a Runnable which may get cancelled via ScheduledFuture. What this means is that whatever you’re doing in your action, you need to be able to catch InterruptedException which is raised if task is cancelled.

10.7.2 Transition Action Error Handling

User can always catch exceptions manually but with actions defined for transitions it is possible to define error action which is called if exception is raised. Exception is then available from a StateContext passed to that action.

@Configuration
@EnableStateMachine
public class Config53
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(States.S1)
                .target(States.S2)
                .event(Events.E1)
                .action(action(), errorAction());
    }

    @Bean
    public Action<States, Events> action() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                throw new RuntimeException("MyError");
            }
        };
    }

    @Bean
    public Action<States, Events> errorAction() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                // RuntimeException("MyError") added to context
                Exception exception = context.getException();
                exception.getMessage();
            }
        };
    }

}

Similar logic can be done manually for every action if needed.

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
        throws Exception {
    transitions
        .withExternal()
            .source(States.S1)
            .target(States.S2)
            .event(Events.E1)
            .action(Actions.errorCallingAction(action(), errorAction()));
}

10.7.3 State Action Error Handling

Similar logic for error handling what is available for transition actions is also available for actions defined for state behaviour and its entry and exit.

For these StateConfigurer has methods stateEntry, stateDo and stateExit to define error action together with an actual action.

@Configuration
@EnableStateMachine
public class Config55
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1)
                .stateEntry(States.S2, action(), errorAction())
                .stateDo(States.S2, action(), errorAction())
                .stateExit(States.S2, action(), errorAction())
                .state(States.S3);
    }

    @Bean
    public Action<States, Events> action() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                throw new RuntimeException("MyError");
            }
        };
    }

    @Bean
    public Action<States, Events> errorAction() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                // RuntimeException("MyError") added to context
                Exception exception = context.getException();
                exception.getMessage();
            }
        };
    }
}

10.8 Configuring Pseudo States

Pseudo state configuration is usually done by configuring states and transitions. Pseudo states are automatically added to state machine as states.

10.8.1 Initial State

Simply mark a particular state as initial state by using initial() method. There are two methods where one takes extra argument to define an initial action. This initial action is good for example initialize extended state variables.

@Configuration
@EnableStateMachine
public class Config11
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1, initialAction())
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

    @Bean
    public Action<States, Events> initialAction() {
        return new Action<States, Events>() {

            @Override
            public void execute(StateContext<States, Events> context) {
                // do something initially
            }
        };
    }

}

10.8.2 Terminate State

Simply mark a particular state as end state by using end() method. This can be done max one time per individual sub-machine or region.

@Configuration
@EnableStateMachine
public class Config1Enums
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.S1)
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

}

10.8.3 History State

History state can be defined once for each individual state machine. You need to choose its state identifier and History.SHALLOW or History.DEEP respectively.

@Configuration
@EnableStateMachine
public class Config12
        extends EnumStateMachineConfigurerAdapter<States3, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States3, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States3.S1)
                .state(States3.S2)
                .and()
                .withStates()
                    .parent(States3.S2)
                    .initial(States3.S2I)
                    .state(States3.S21)
                    .state(States3.S22)
                    .history(States3.SH, History.SHALLOW);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States3, Events> transitions)
            throws Exception {
        transitions
            .withHistory()
                .source(States3.SH)
                .target(States3.S22);
    }

}

Also as shown above, optionally it is possible to define a default transition from a history state into a state vertex in a same machine. This transition takes place as a default if for example machine has never been entered, thus no history would be available. If default state transition is not defined, then normal entry into a region is done. This default transition is also used if machine’s history is a final state.

10.8.4 Choice State

Choice needs to be defined in both states and transitions to work properly. Mark particular state as choice state by using choice() method. This state needs to match source state when transition is configured for this choice.

Transition is configured using withChoice() where you define source state and first/then/last structure which is equivalent to normal if/elseif/else. With first and then you can specify a guard just like you’d use a condition with if/elseif clauses.

Transition needs to be able to exist so make sure last is used. Otherwise configuration is ill-formed.

@Configuration
@EnableStateMachine
public class Config13
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.SI)
                .choice(States.S1)
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withChoice()
                .source(States.S1)
                .first(States.S2, s2Guard())
                .then(States.S3, s3Guard())
                .last(States.S4);
    }

    @Bean
    public Guard<States, Events> s2Guard() {
        return new Guard<States, Events>() {

            @Override
            public boolean evaluate(StateContext<States, Events> context) {
                return false;
            }
        };
    }

    @Bean
    public Guard<States, Events> s3Guard() {
        return new Guard<States, Events>() {

            @Override
            public boolean evaluate(StateContext<States, Events> context) {
                return true;
            }
        };
    }

}

Actions can be executed with both incoming and outgoing transitions of a choice pseudostate. As seeing from below example, one dummy lambda action is defined leading into a choice state and one similar dummy lambda action defined for one outgoing transition where it also define an error action.

@Configuration
@EnableStateMachine
public class Config23
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.SI)
                .choice(States.S1)
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(States.SI)
                .action(c -> {
                        // action with SI-S1
                    })
                .target(States.S1)
                .and()
            .withChoice()
                .source(States.S1)
                .first(States.S2, c -> {
                        return true;
                    })
                .last(States.S3, c -> {
                        // action with S1-S3
                    }, c -> {
                        // error callback for action S1-S3
                    });
    }
}
[Note]Note

Junction have same api format meaning actions can be defined similarly.

10.8.5 Junction State

Junction needs to be defined in both states and transitions to work properly. Mark particular state as choice state by using junction() method. This state needs to match source state when transition is configured for this choice.

Transition is configured using withJunction() where you define source state and first/then/last structure which is equivalent to normal if/elseif/else. With first and then you can specify a guard just like you’d use a condition with if/elseif clauses.

Transition needs to be able to exist so make sure last is used. Otherwise configuration is ill-formed.

@Configuration
@EnableStateMachine
public class Config20
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States.SI)
                .junction(States.S1)
                .end(States.SF)
                .states(EnumSet.allOf(States.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
            throws Exception {
        transitions
            .withJunction()
                .source(States.S1)
                .first(States.S2, s2Guard())
                .then(States.S3, s3Guard())
                .last(States.S4);
    }

    @Bean
    public Guard<States, Events> s2Guard() {
        return new Guard<States, Events>() {

            @Override
            public boolean evaluate(StateContext<States, Events> context) {
                return false;
            }
        };
    }

    @Bean
    public Guard<States, Events> s3Guard() {
        return new Guard<States, Events>() {

            @Override
            public boolean evaluate(StateContext<States, Events> context) {
                return true;
            }
        };
    }

}
[Note]Note

Difference between choice and junction is purely academic as both are implemented with first/then/last structure. However in theory based on uml model, choice allows only one incoming transition while junction allows multiple incoming transitions. At a code level functionality is pretty much identical.

10.8.6 Fork State

Fork needs to be defined in both states and transitions to work properly. Mark particular state as choice state by using fork() method. This state needs to match source state when transition is configured for this fork.

Target state needs to be a super state or immediate states in regions. Using a super state as target will take all regions into initial states. Targeting individual state give more controlled entry into regions.

@Configuration
@EnableStateMachine
public class Config14
        extends EnumStateMachineConfigurerAdapter<States2, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States2, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States2.S1)
                .fork(States2.S2)
                .state(States2.S3)
                .and()
                .withStates()
                    .parent(States2.S3)
                    .initial(States2.S2I)
                    .state(States2.S21)
                    .state(States2.S22)
                    .end(States2.S2F)
                    .and()
                .withStates()
                    .parent(States2.S3)
                    .initial(States2.S3I)
                    .state(States2.S31)
                    .state(States2.S32)
                    .end(States2.S3F);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States2, Events> transitions)
            throws Exception {
        transitions
            .withFork()
                .source(States2.S2)
                .target(States2.S22)
                .target(States2.S32);
    }

}

10.8.7 Join State

Join needs to be defined in both states and transitions to work properly. Mark particular state as choice state by using join() method. This state doesn’t need to match either source states or target state in a transition configuration.

Select a target state where transition goes when all source states has been joined. If you use state hosting regions as source, end states of a regions are used as joins. Otherwise you can pick any states from a regions.

@Configuration
@EnableStateMachine
public class Config15
        extends EnumStateMachineConfigurerAdapter<States2, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States2, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States2.S1)
                .state(States2.S3)
                .join(States2.S4)
                .state(States2.S5)
                .and()
                .withStates()
                    .parent(States2.S3)
                    .initial(States2.S2I)
                    .state(States2.S21)
                    .state(States2.S22)
                    .end(States2.S2F)
                    .and()
                .withStates()
                    .parent(States2.S3)
                    .initial(States2.S3I)
                    .state(States2.S31)
                    .state(States2.S32)
                    .end(States2.S3F);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States2, Events> transitions)
            throws Exception {
        transitions
            .withJoin()
                .source(States2.S2F)
                .source(States2.S3F)
                .target(States2.S4)
                .and()
            .withExternal()
                .source(States2.S4)
                .target(States2.S5);
    }
}

It is also possible to have multiple transitions originating from a join state. It this case it is advised to use guards and define those so that only one guard evaluates TRUE at any given time as otherwise transition behaviour is not predicted. This is shown above where guard simply checks if extended state has variables.

@Configuration
@EnableStateMachine
public class Config22
        extends EnumStateMachineConfigurerAdapter<States2, Events> {

    @Override
    public void configure(StateMachineStateConfigurer<States2, Events> states)
            throws Exception {
        states
            .withStates()
                .initial(States2.S1)
                .state(States2.S3)
                .join(States2.S4)
                .state(States2.S5)
                .end(States2.SF)
                .and()
                .withStates()
                    .parent(States2.S3)
                    .initial(States2.S2I)
                    .state(States2.S21)
                    .state(States2.S22)
                    .end(States2.S2F)
                    .and()
                .withStates()
                    .parent(States2.S3)
                    .initial(States2.S3I)
                    .state(States2.S31)
                    .state(States2.S32)
                    .end(States2.S3F);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States2, Events> transitions)
            throws Exception {
        transitions
            .withJoin()
                .source(States2.S2F)
                .source(States2.S3F)
                .target(States2.S4)
                .and()
            .withExternal()
                .source(States2.S4)
                .target(States2.S5)
                .guardExpression("!extendedState.variables.isEmpty()")
                .and()
            .withExternal()
                .source(States2.S4)
                .target(States2.SF)
                .guardExpression("extendedState.variables.isEmpty()");
    }
}

10.8.8 Exit/Entry Point States

Exit and Entry Points can be used to do more controlled exit and entry from and into a submachines.

@Configuration
@EnableStateMachine
static class Config21 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
        .withStates()
            .initial("S1")
            .state("S2")
            .state("S3")
            .and()
            .withStates()
                .parent("S2")
                .initial("S21")
                .entry("S2ENTRY")
                .exit("S2EXIT")
                .state("S22");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
        .withExternal()
            .source("S1").target("S2")
            .event("E1")
            .and()
        .withExternal()
            .source("S1").target("S2ENTRY")
            .event("ENTRY")
            .and()
        .withExternal()
            .source("S22").target("S2EXIT")
            .event("EXIT")
            .and()
        .withEntry()
            .source("S2ENTRY").target("S22")
            .and()
        .withExit()
            .source("S2EXIT").target("S3");
    }
}

As shown above you need to mark particular states as exit and entry states. Then you create a normal transitions into those states and also specify withExit() and withEntry() where those states will exit and entry respectively.

10.9 Configuring Common Settings

Some of a common state machine configuration can be set via a ConfigurationConfigurer. This allows to set BeanFactory, TaskExecutor, TaskScheduler, autostart flag for a state machine and register StateMachineListener instances.

@Configuration
@EnableStateMachine
public class Config17
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
            throws Exception {
        config
            .withConfiguration()
                .autoStartup(true)
                .beanFactory(new StaticListableBeanFactory())
                .taskExecutor(new SyncTaskExecutor())
                .taskScheduler(new ConcurrentTaskScheduler())
                .listener(new StateMachineListenerAdapter<States, Events>());
    }
}

State machine autoStartup flag is disabled by default because all instances handling sub-states are controlled by a state machine itself and cannot be started automatically. Also it is much safer to leave this decision to a user whether a machine should be started automatically or not. This flag will only control an autostart of a top-level state machine.

Setting a BeanFactory, TaskExecutor or TaskScheduler exist for convenience for a user and are also use within a framework itself.

Registering StateMachineListener instances is also partly for convenience but is required if user wants to catch callback during a state machine lifecycle like getting notified of a state machine start/stop events. Naturally it is not possible to listen a state machine start events if autoStartup is enabled unless listener can be registered during a configuration phase.

DistributedStateMachine is configured via withDistributed() which allows to set a StateMachineEnsemble which if exists automatically wraps created StateMachine with DistributedStateMachine and enables distributed mode.

@Configuration
@EnableStateMachine
public class Config18
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
            throws Exception {
        config
            .withDistributed()
                .ensemble(stateMachineEnsemble());
    }

    @Bean
    public StateMachineEnsemble<States, Events> stateMachineEnsemble()
            throws Exception {
        // naturally not null but should return ensemble instance
        return null;
    }
}

More about distributed states, refer to section Chapter 29, Using Distributed States.

StateMachineModelVerifier is an interface what is used internally to do some sanity checks for a state machine structure. Its purpose is to fail fast early instead of letting common configuration errors into a state machine itself. On default verifier is automatically enabled and DefaultStateMachineModelVerifier implementation is used.

With withVerifier() user can disable verifier or set a custom one if needed.

@Configuration
@EnableStateMachine
public class Config19
        extends EnumStateMachineConfigurerAdapter<States, Events> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<States, Events> config)
            throws Exception {
        config
            .withVerifier()
                .enabled(true)
                .verifier(verifier());
    }

    @Bean
    public StateMachineModelVerifier<States, Events> verifier() {
        return new StateMachineModelVerifier<States, Events>() {

            @Override
            public void verify(StateMachineModel<States, Events> model) {
                // throw exception indicating malformed model
            }
        };
    }
}

More about config model, refer to section Section 52.1, “StateMachine Config Model”.

10.10 Configuring Model

StateMachineModelFactory is a hook to configure statemachine model without using a manual configuration. Essentially it is a third party integration to integrate into a configuration model. StateMachineModelFactory can be hooked into a configuration model by using a StateMachineModelConfigurer as shown above.

@Configuration
@EnableStateMachine
public static class Config1 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineModelConfigurer<String, String> model) throws Exception {
        model
            .withModel()
                .factory(modelFactory());
    }

    @Bean
    public StateMachineModelFactory<String, String> modelFactory() {
        return new CustomStateMachineModelFactory();
    }
}

As a custom example CustomStateMachineModelFactory would simply define two states, S1 and S2 and an event E1 between those states.

public static class CustomStateMachineModelFactory implements StateMachineModelFactory<String, String> {

    @Override
    public StateMachineModel<String, String> build() {
        ConfigurationData<String, String> configurationData = new ConfigurationData<>();
        Collection<StateData<String, String>> stateData = new ArrayList<>();
        stateData.add(new StateData<String, String>("S1", true));
        stateData.add(new StateData<String, String>("S2"));
        StatesData<String, String> statesData = new StatesData<>(stateData);
        Collection<TransitionData<String, String>> transitionData = new ArrayList<>();
        transitionData.add(new TransitionData<String, String>("S1", "S2", "E1"));
        TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData);
        StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<String, String>(configurationData,
                statesData, transitionsData);
        return stateMachineModel;
    }

    @Override
    public StateMachineModel<String, String> build(String machineId) {
        return build();
    }
}
[Note]Note

Defining a custom model is usually not what end user is looking for, although it is possible, however it is a central concept of allowing external access to this configuration model.

Example of using this model factory integration can be found from Chapter 31, Eclipse Modeling Support. More generic info about custom model integration can be found from Chapter 52, Developer Documentation.

10.11 Things to Remember

When defining actions, guards or any other references from a configuration there are things to remember how Spring Framework works with beans. In below we have defined a normal configuration with states S1 and S2 and 4 transitions between those. All transitions are either guarded by guard1 or guard2. Pay attention that guard1 is created as a real bean because it’s annotated with a @Bean, while guard2 is not.

What this mean is that event E3 would get guard2 condition as TRUE and E4 would get guard2 condition as FALSE as those are simply coming from a plain method calls to those functions.

However because guard1 is defined as a @Bean, it is proxied by a Spring Framework, thus additional calls to its method will result only one instantiation of that instance. Event E1 would get first proxied instance with condition TRUE while event E2 would get same instance with TRUE condition while method call was defined with FALSE. This is not a Spring State Machine specific behaviour, it’s just how Spring Framework works with Beans.

@Configuration
@EnableStateMachine
public class Config1
        extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
            .withStates()
                .initial("S1")
                .state("S2");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source("S1").target("S2").event("E1").guard(guard1(true))
                .and()
            .withExternal()
                .source("S1").target("S2").event("E2").guard(guard1(false))
                .and()
            .withExternal()
                .source("S1").target("S2").event("E3").guard(guard2(true))
                .and()
            .withExternal()
                .source("S1").target("S2").event("E4").guard(guard2(false));
    }

    @Bean
    public Guard<String, String> guard1(final boolean value) {
        return new Guard<String, String>() {
            @Override
            public boolean evaluate(StateContext<String, String> context) {
                return value;
            }
        };
    }

    public Guard<String, String> guard2(final boolean value) {
        return new Guard<String, String>() {
            @Override
            public boolean evaluate(StateContext<String, String> context) {
                return value;
            }
        };
    }
}