How to properly handle errors in Spring StateMachine

Lejdi Prifti
4 min readDec 2, 2023

In this article, we are going to look at the proper way of handling errors in Spring StateMachine.

A state machine is a conceptual model that describes a system’s behavior through a finite set of states, transitions between those states, and the events that cause those changes. It is used in computer science and engineering. It is a popular abstraction for software development, control systems, and business processes, among other disciplines. It is a potent representation of dynamic systems.

One of the most important aspects of state machine is error handling, which is dealing with unforeseen problems or anomalies that might arise when a program is being executed. It is a crucial component of building software systems that are stable and dependable.

Errors can appear in a number of ways during the software development process, including unexpected inputs, logical problems, and runtime exceptions. To maintain the software’s smooth operation, effective error handling attempts to identify, report, and, if feasible, gracefully recover from these problems.

Setup & Configuration

Let’s start with adding the Spring StateMachine dependency in our project.

<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>

Next, we need to setup our States and Events enums.

public enum States {
DRAFT, REVIEW, SUBMITTED_TO_CLIENT, APPROVED
}

public enum Events {
BEGIN_REVIEW, SUBMIT, APPROVE
}

It is now important to create a configuration class named StateMachineConfig which extends StateMachineConfigurerAdapter and defines our states, events, transitions, and any necessary actions.

@Configuration
@EnableStateMachineFactory
public class StateMachineConfig
extends StateMachineConfigurerAdapter<States, Events> {
// basic setup
}

The first method that we will override and provide our own implementation is configure that accepts a StateMachineStateConfigurer object. In this method, we define the initial state and all the possible states of the

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

Very good! Now that the state has been established, we will specify the transitions. In essence, a transition is the change in state brought about by an external event. To configure the transitions, we will implement the method named as well configure but accepts a StateMachineTransitionConfigurer object.

@Override
public void configure(StateMachineTransitionConfigurer<States, Events>
transitions) throws Exception {
transitions
.withExternal()
.source(States.DRAFT)
.target(States.REVIEW)
.event(Events.BEGIN_REVIEW)
.and()
.withExternal()
.source(States.REVIEW)
.target(States.SUBMITTED_TO_CLIENT)
.event(Events.SUBMIT)
.and()
.withExternal()
.source(States.SUBMITTED_TO_CLIENT)
.target(States.APPROVED)
.event(Events.APPROVE);
}

During each transition, we can execute actions.

@Override
public void configure(StateMachineTransitionConfigurer<States, Events>
transitions) throws Exception {
transitions
.withExternal()
.source(States.DRAFT)
.target(States.REVIEW)
.action(new FromDraftToReviewAction())
.event(Events.BEGIN_REVIEW)
.and()
.withExternal()
.source(States.REVIEW)
.target(States.SUBMITTED_TO_CLIENT)
.action(new FromReviewToSubmmitedToClientAction())
.event(Events.SUBMIT)
.and()
.withExternal()
.source(States.SUBMITTED_TO_CLIENT)
.target(States.APPROVED)
.action(new FromSubmittedToClientToApprovedAction())
.event(Events.APPROVE);
}

Let’s see the implementation of one of the actions. We will throw an Exception to simulate an action going wrong.

@Slf4j
public class FromDraftToReviewAction implements Action<States, Events> {

@Override
public void execute(StateContext<States, Events> context) {
try {
throw new Exception("state machine threw an exception");
} catch (Exception e) {
log.error("failed converting from status {} to status {}",
context.getSource().getId(),
context.getTarget().getId());
context.getExtendedState().getVariables().put(SharedConstants.ERROR, e);
}
}
}

Error Handling

Now, we look at the topic you’re here for. As you can see from the code above, we catch the exception and put it as a variable in the extended state of the StateContext .

As written before, each transition is triggered by sending an event. Let’s send one.

After sending the event, we make an important check if the statemachine object reference has any errors in the extended state. If it does, we will throw it and our controller will respond with a 500 Internal Server Error message.


@Service
public class TransactionService {

private final StateMachine<States, Events> stateMachine;

public TransactionService(StateMachineFactory<States, Events> stateMachineFactory) {
this.stateMachine = stateMachineFactory.getStateMachine();
}

public void triggerEvent(Events event) throws Exception {
stateMachine.sendEvent(event);
Object error = sm.getExtendedState().getVariables()
.get(SharedConstants.ERROR);
if (error != null) {
throw Exception.class.cast(error);
}

}
}

Using our TransactionService , we can trigger the event BEING_REVIEW and test it. Due to the error we simulated in the FromDraftToReviewAction , the server will respond with 500 Internal Server Error .

@RestController
@RequestMapping("/api")
public class TransactionController {

private final TransactionService transactionService;

public TransactionController(TransactionService transactionService) {
this.transactionService = transactionService;
}

@GetMapping("/trigger-event/{event}")
public String triggerEvent(@PathVariable String event) throws Exception {
Events eventEnum = Events.valueOf(event.toUpperCase());
transactionService.triggerEvent(eventEnum);
return "Event triggered successfully: " + eventEnum;
}
}

Note: I have used Exception in this article, but I advise not using generic exceptions. Go for domain exceptions that are more readable and less confusing.

Thank you for reading!

--

--

Lejdi Prifti

Software Developer | ML Enthusiast | AWS Practitioner | Kubernetes Administrator