Lejdi Prifti

0 %
Lejdi Prifti
Software Engineer
Web3 Developer
ML Practitioner
  • Residence:
    Albania
  • City:
    Tirana
  • Email:
    info@lejdiprifti.com
English
Italian
French
Spring
React & Angular
Machine Learning
Docker & Kubernetes
AWS & Cloud
Team Player
Communication
Time Management
  • Java, JavaScript, Python
  • AWS, Kubernetes, Azure
  • Bootstrap, Materialize
  • Css, Sass, Less
  • Blockchain, Ethereum, Solidity
  • React, React Native, Flutter
  • GIT knowledge
  • Machine Learning, Deep Learning
0

No products in the basket.

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.

				
					<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 specific actions. For example, when the state passes from DRAFT to REVIEW we are executing the action FromDraftToReviewAction.

				
					@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);
      }
    }
}
				
			

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<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.

Buy Me A Coffee
Posted in SpringBootTags:
Write a comment
Receive the latest articles

Subscribe To My Weekly Newsletter

Get insights, practical tips, and career advice to advance your skills and stay ahead in the tech industry.