Back to Spring Boot Course

Spring Boot Course

In Spring Boot, a state machine is a design pattern used to manage the state transitions of an object. The state machine pattern is especially useful for modeling workflows, business processes, and any scenario where an object can be in different states with well-defined transitions between those states.

Spring Boot provides support for state machines through the Spring Statemachine project. This project offers a framework to define, control, and persist state machines.

Key Concepts of State Machines

  • State: Represents a condition or situation during the life of an object. For example, in a booking management system, states could be NEW, CONFIRMED, CANCELLED, etc.

  • Event: Triggers that cause transitions between states. For example, an event could be CONFIRM, CANCEL.

  • Transition: The change from one state to another, usually triggered by an event.

  • Action: A piece of code that executes during a transition, state entry, or state exit.

  • Guard: A condition that must be true for a transition to occur.

Example

Let's implement state machine in booking management system.

Dependencies

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>4.0.0</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

Define States and Events

package com.praveen.booking.model; public enum BookingStates { NEW, CONFIRMED, CANCELLED }
package com.praveen.booking.model; public enum BookingEvents { CONFIRM, CANCEL }

Create the State Machine Configuration

package com.praveen.booking.config; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import com.praveen.booking.model.BookingEvents; import com.praveen.booking.model.BookingStates; @Configuration @EnableStateMachine public class StateMachineConfig extends StateMachineConfigurerAdapter<BookingStates, BookingEvents> { @Override public void configure(StateMachineStateConfigurer<BookingStates, BookingEvents> states) throws Exception { states.withStates().initial(BookingStates.NEW).state(BookingStates.NEW).state(BookingStates.CONFIRMED) .state(BookingStates.CANCELLED); } @Override public void configure(StateMachineTransitionConfigurer<BookingStates, BookingEvents> transitions) throws Exception { transitions.withExternal().source(BookingStates.NEW).target(BookingStates.CONFIRMED) .event(BookingEvents.CONFIRM).and().withExternal().source(BookingStates.NEW) .target(BookingStates.CANCELLED).event(BookingEvents.CANCEL); } }

Create the Booking Entity

package com.praveen.booking.entity; import com.praveen.booking.model.BookingStates; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @Entity @AllArgsConstructor @NoArgsConstructor @Data @SuperBuilder public class Booking { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private BookingStates state; }

Create the Booking Repository

package com.praveen.booking.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.praveen.booking.entity.Booking; public interface BookingRepository extends JpaRepository<Booking, Long> { }

Create the Booking Service

package com.praveen.booking.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.support.DefaultStateMachineContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.praveen.booking.entity.Booking; import com.praveen.booking.model.BookingEvents; import com.praveen.booking.model.BookingStates; import com.praveen.booking.repository.BookingRepository; @Service public class BookingService { @Autowired private BookingRepository bookingRepository; @Autowired private StateMachine<BookingStates, BookingEvents> stateMachine; @Transactional public Booking createBooking() { Booking booking = new Booking(); booking.setState(BookingStates.NEW); return bookingRepository.save(booking); } @Transactional public Booking confirmBooking(Long bookingId) { Booking booking = bookingRepository.findById(bookingId).orElseThrow(); stateMachine.stop(); stateMachine.getStateMachineAccessor().doWithAllRegions(access -> access .resetStateMachine(new DefaultStateMachineContext<>(booking.getState(), null, null, null))); stateMachine.start(); stateMachine.sendEvent(BookingEvents.CONFIRM); booking.setState(stateMachine.getState().getId()); return bookingRepository.save(booking); } @Transactional public Booking cancelBooking(Long bookingId) { Booking booking = bookingRepository.findById(bookingId).orElseThrow(); stateMachine.stop(); stateMachine.getStateMachineAccessor().doWithAllRegions(access -> access .resetStateMachine(new DefaultStateMachineContext<>(booking.getState(), null, null, null))); stateMachine.start(); stateMachine.sendEvent(BookingEvents.CANCEL); booking.setState(stateMachine.getState().getId()); return bookingRepository.save(booking); } }

Create the Booking Controller

package com.praveen.booking.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.praveen.booking.entity.Booking; import com.praveen.booking.service.BookingService; @RestController @RequestMapping("/bookings") public class BookingController { @Autowired private BookingService bookingService; @PostMapping public Booking createBooking() { return bookingService.createBooking(); } @PostMapping("/{id}/confirm") public Booking confirmBooking(@PathVariable Long id) { return bookingService.confirmBooking(id); } @PostMapping("/{id}/cancel") public Booking cancelBooking(@PathVariable Long id) { return bookingService.cancelBooking(id); } }

Lets open swagger http://localhost:8080/swagger-ui/index.html to test the endpoints.

Create two new bookings POST /bookings

{ "id": 1, "state": "NEW" }
{ "id": 2, "state": "NEW" }

Confirm a booking POST /bookings/1/confirm

{ "id": 1, "state": "CONFIRMED" }

Cancel a booking POST /bookings/2/cancel

{ "id": 2, "state": "CANCELLED" }