Home » How to properly handle errors in Spring StateMachine
How to properly handle errors in Spring StateMachine
2. December 2023
In this article, we are going to look at the proper way of handling errors in Spring StateMachine.
Table of Contents
Overview
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
We start by adding the Spring StateMachine dependency in our project.
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 {
// 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) 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.
During each transition, we can execute specific actions. For example, when the state passes from DRAFT to REVIEW we are executing the action FromDraftToReviewAction.
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 {
@Override
public void execute(StateContext 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);
}
}
}
StateMachine 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.
After triggering the transition, 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 stateMachine;
public TransactionService(StateMachineFactory 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.