├── .gitignore ├── src └── main │ ├── java │ └── com │ │ └── gcalsolaro │ │ └── statemachine │ │ ├── domain │ │ ├── enums │ │ │ ├── UserState.java │ │ │ ├── InstanceState.java │ │ │ └── statemachine │ │ │ │ ├── ApplicationEvents.java │ │ │ │ └── ApplicationStates.java │ │ └── entity │ │ │ ├── Instance.java │ │ │ └── User.java │ │ ├── repository │ │ ├── UserRepository.java │ │ └── InstanceRepository.java │ │ ├── Application.java │ │ ├── config │ │ ├── StateMachineListener.java │ │ └── StateMachineConfiguration.java │ │ ├── controller │ │ └── ApiController.java │ │ └── service │ │ ├── SagaOrchestrator.java │ │ └── SagaService.java │ └── resources │ ├── logback.xml │ ├── application.yml │ └── schema.sql ├── .github └── workflows │ └── maven.yml ├── README.md └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings/ 2 | /target/ 3 | /.classpath 4 | /.project 5 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/domain/enums/UserState.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.domain.enums; 2 | 3 | public enum UserState { 4 | 5 | NEW, CREATED, REJECTED 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/domain/enums/InstanceState.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.domain.enums; 2 | 3 | public enum InstanceState { 4 | 5 | NEW, PENDING, CREATED, REJECTED 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.repository; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | 5 | import com.gcalsolaro.statemachine.domain.entity.User; 6 | 7 | public interface UserRepository extends PagingAndSortingRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/domain/enums/statemachine/ApplicationEvents.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.domain.enums.statemachine; 2 | 3 | public enum ApplicationEvents { 4 | 5 | CREATE_INSTANCE, 6 | CREATE_USER, 7 | UPDATE_USER_CREATED, 8 | UPDATE_USER_REJECTED, 9 | UPDATE_INSTANCE_CREATED, 10 | UPDATE_INSTANCE_REJECTED 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/domain/enums/statemachine/ApplicationStates.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.domain.enums.statemachine; 2 | 3 | public enum ApplicationStates { 4 | 5 | START_INSTANCE, 6 | INSTANCE_PENDING, 7 | START_USER, 8 | EVALUATE_USER_PENDING, 9 | END_USER_OK, 10 | END_USER_KO, 11 | END_INSTANCE_OK, 12 | END_INSTANCE_KO 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/repository/InstanceRepository.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.repository; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | 5 | import com.gcalsolaro.statemachine.domain.entity.Instance; 6 | 7 | public interface InstanceRepository extends PagingAndSortingRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | h2: console: enabled: true 6 | path: /h2-console statemachine: jpa: 7 | driver-class-name: org.h2.Driver 8 | url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 9 | username: SA 10 | password: jpa: 11 | show-sql: true hibernate: ddl-auto: create-drop 12 | properties: 13 | hibernate: 14 | dialect: org.hibernate.dialect.H2Dialect -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/Application.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Import; 6 | 7 | import com.gcalsolaro.statemachine.config.StateMachineConfiguration; 8 | 9 | @SpringBootApplication 10 | @Import(StateMachineConfiguration.class) 11 | public class Application { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | } -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '11' 23 | distribution: 'adopt' 24 | - name: Build with Maven 25 | run: mvn -B package --file pom.xml 26 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | /*DROP TABLE IF EXISTS user, instance;*/ 2 | 3 | CREATE TABLE user ( 4 | id_user number NOT NULL, 5 | c_fiscal_code varchar(16) NOT NULL, 6 | name varchar(64) NULL, 7 | surname varchar(64) NULL, 8 | dt_born date NULL, 9 | state varchar(20) NOT NULL, 10 | email varchar(255) NULL, 11 | CONSTRAINT user_c_fiscal_code_key UNIQUE (c_fiscal_code), 12 | CONSTRAINT user_pkey PRIMARY KEY (id_user) 13 | ); 14 | 15 | CREATE TABLE instance ( 16 | id_instance number NOT NULL, 17 | c_instance varchar(45) NOT NULL, 18 | fk_user int4, 19 | info varchar(255) NULL, 20 | state varchar(20) NOT NULL, 21 | CONSTRAINT instance_c_instance_key UNIQUE (c_instance), 22 | CONSTRAINT instance_pkey PRIMARY KEY (id_instance) 23 | ); 24 | 25 | ALTER TABLE instance ADD CONSTRAINT fk_instance_user FOREIGN KEY (fk_user) REFERENCES user(id_user); -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/config/StateMachineListener.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.config; 2 | 3 | import org.springframework.statemachine.listener.StateMachineListenerAdapter; 4 | import org.springframework.statemachine.state.State; 5 | 6 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationEvents; 7 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationStates; 8 | 9 | import java.util.logging.Logger; 10 | 11 | public class StateMachineListener extends StateMachineListenerAdapter { 12 | 13 | private static final Logger logger = Logger.getLogger(StateMachineListener.class.getName()); 14 | 15 | @Override 16 | public void stateChanged(State from, State to) { 17 | logger.info(() -> String.format("Transitioned from %s to %s%n", from == null ? "none" : from.getId(), to.getId())); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot Statemachine 2 | 3 | ![Java CI with Maven](https://github.com/gcalsolaro/spring-boot-state-machine/workflows/Java%20CI%20with%20Maven/badge.svg) 4 | > **Sample application using Statemachine powered by Spring** 5 | 6 | 7 | ## Table of Contents 8 | 9 | * [Spring Boot Statemachine](#spring-boot-statemachine) 10 | * [Architecture](#architecture) 11 | * [Prerequisites](#prerequisites) 12 | * [Rest API](#rest-api) 13 | * [Statemachine Rest API](#statemachine-rest-api) 14 | 15 | 16 | ## Architecture 17 | 18 | Microservice architectural style is an approach to developing a single application as a suite of small services. 19 | In this example we use spring statemachine to manage distributed transactions (two phase commit) by implementing the logic of the SAGA Pattern. 20 | The technology stack used is provided by Spring, in particular: 21 | 22 | * **_Spring Boot_** - 2.0.0.RELEASE 23 | * **_Spring State Machine Core_** - 1.2.3.RELEASE 24 | * **_Spring Data JPA with Hibernate_** - 2.0.0.RELEASE 25 | * **_H2 Database Engine_** - 1.4.196 26 | 27 | ## Prerequisites 28 | * **_JDK 8_** - Install JDK 1.8 version 29 | * **_Maven_** - Download latest version 30 | 31 | 32 | 33 | ## Rest API 34 | 35 | ### Statemachine Rest API 36 | 37 | Method | URI | Description | Parameters | 38 | --- | --- | --- | --- | 39 | `GET` | */api/statemachine/start/ok* | SAGA Pattern end with Instance CREATED 40 | `GET` | */api/statemachine/start/ko* | SAGA Pattern end with Instance REJECTED 41 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/controller/ApiController.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.controller; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.statemachine.StateMachine; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationEvents; 15 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationStates; 16 | import com.gcalsolaro.statemachine.service.SagaOrchestrator; 17 | 18 | @RestController 19 | @RequestMapping("/api/statemachine") 20 | public class ApiController { 21 | 22 | @Autowired 23 | private SagaOrchestrator sagaOrchestrator; 24 | 25 | /** 26 | * Test for ok 27 | * 28 | * @param request 29 | * @param response 30 | * @return 31 | */ 32 | @GetMapping("/start/ok") 33 | public ResponseEntity ok(HttpServletRequest request, HttpServletResponse response) { 34 | StateMachine sagaResult = sagaOrchestrator.createInstanceSaga(true); 35 | return new ResponseEntity(sagaResult.getState().getId().name(), HttpStatus.OK); 36 | } 37 | 38 | /** 39 | * Test for ko 40 | * 41 | * @param request 42 | * @param response 43 | * @return 44 | */ 45 | @GetMapping("/start/ko") 46 | public ResponseEntity ko(HttpServletRequest request, HttpServletResponse response) { 47 | StateMachine sagaResult = sagaOrchestrator.createInstanceSaga(false); 48 | return new ResponseEntity(sagaResult.getState().getId().name(), HttpStatus.OK); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/domain/entity/Instance.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.domain.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.EnumType; 8 | import javax.persistence.Enumerated; 9 | import javax.persistence.FetchType; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import javax.persistence.JoinColumn; 14 | import javax.persistence.ManyToOne; 15 | 16 | import com.gcalsolaro.statemachine.domain.enums.InstanceState; 17 | 18 | /** 19 | * The persistent class for the instance database table. 20 | * 21 | */ 22 | @Entity 23 | public class Instance implements Serializable { 24 | 25 | private static final long serialVersionUID = 1L; 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | @Column(name = "id_instance") 30 | private Integer idInstance; 31 | 32 | @Column(name = "c_instance") 33 | private String cInstance; 34 | 35 | private String info; 36 | 37 | @Column 38 | @Enumerated(EnumType.STRING) 39 | private InstanceState state; 40 | 41 | // bi-directional many-to-one association to User 42 | @ManyToOne(fetch = FetchType.LAZY) 43 | @JoinColumn(name = "fk_user") 44 | private User user; 45 | 46 | public Instance() { 47 | } 48 | 49 | public Integer getIdInstance() { 50 | return idInstance; 51 | } 52 | 53 | public void setIdInstance(Integer idInstance) { 54 | this.idInstance = idInstance; 55 | } 56 | 57 | public String getcInstance() { 58 | return cInstance; 59 | } 60 | 61 | public void setcInstance(String cInstance) { 62 | this.cInstance = cInstance; 63 | } 64 | 65 | public String getInfo() { 66 | return info; 67 | } 68 | 69 | public void setInfo(String info) { 70 | this.info = info; 71 | } 72 | 73 | public InstanceState getState() { 74 | return state; 75 | } 76 | 77 | public void setState(InstanceState state) { 78 | this.state = state; 79 | } 80 | 81 | public User getUser() { 82 | return user; 83 | } 84 | 85 | public void setUser(User user) { 86 | this.user = user; 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/domain/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.domain.entity; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.EnumType; 9 | import javax.persistence.Enumerated; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.GenerationType; 12 | import javax.persistence.Id; 13 | import javax.persistence.Temporal; 14 | import javax.persistence.TemporalType; 15 | 16 | import com.gcalsolaro.statemachine.domain.enums.UserState; 17 | 18 | /** 19 | * The persistent class for the user database table. 20 | * 21 | */ 22 | @Entity 23 | public class User implements Serializable { 24 | 25 | private static final long serialVersionUID = 1L; 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | @Column(name = "id_user") 30 | private Integer idUser; 31 | 32 | @Column(name = "c_fiscal_code") 33 | private String fiscalCode; 34 | 35 | private String surname; 36 | 37 | @Temporal(TemporalType.DATE) 38 | @Column(name = "dt_born") 39 | private Date dtBorn; 40 | 41 | private String email; 42 | 43 | private String name; 44 | 45 | @Column 46 | @Enumerated(EnumType.STRING) 47 | private UserState state; 48 | 49 | public User() { 50 | } 51 | 52 | public Integer getIdUser() { 53 | return idUser; 54 | } 55 | 56 | public void setIdUser(Integer idUser) { 57 | this.idUser = idUser; 58 | } 59 | 60 | public String getFiscalCode() { 61 | return fiscalCode; 62 | } 63 | 64 | public void setFiscalCode(String fiscalCode) { 65 | this.fiscalCode = fiscalCode; 66 | } 67 | 68 | public String getSurname() { 69 | return surname; 70 | } 71 | 72 | public void setSurname(String surname) { 73 | this.surname = surname; 74 | } 75 | 76 | public Date getDtBorn() { 77 | return dtBorn; 78 | } 79 | 80 | public void setDtBorn(Date dtBorn) { 81 | this.dtBorn = dtBorn; 82 | } 83 | 84 | public String getEmail() { 85 | return email; 86 | } 87 | 88 | public void setEmail(String email) { 89 | this.email = email; 90 | } 91 | 92 | public String getName() { 93 | return name; 94 | } 95 | 96 | public void setName(String name) { 97 | this.name = name; 98 | } 99 | 100 | public UserState getState() { 101 | return state; 102 | } 103 | 104 | public void setState(UserState state) { 105 | this.state = state; 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.gcalsolaro 8 | spring-boot-state-machine 9 | 1.0 10 | 11 | Spring Boot State Machine 12 | Spring Boot State Machine 13 | 14 | 15 | 1.8 16 | 2.0.0.RELEASE 17 | UTF-8 18 | com.gcalsolaro.statemachine.Application 19 | 1.2.3.RELEASE 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-dependencies 27 | ${spring-boot.version} 28 | pom 29 | import 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | 41 | org.springframework.statemachine 42 | spring-statemachine-core 43 | ${spring-statemachine-core.version} 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-jpa 50 | 51 | 52 | 53 | com.h2database 54 | h2 55 | runtime 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-maven-plugin 64 | 65 | 66 | 67 | repackage 68 | 69 | 70 | 71 | 72 | 73 | maven-compiler-plugin 74 | 75 | ${java.version} 76 | ${java.version} 77 | ${project.build.sourceEncoding} 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/service/SagaOrchestrator.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Date; 5 | import java.util.List; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.statemachine.StateMachine; 9 | import org.springframework.statemachine.transition.Transition; 10 | import org.springframework.stereotype.Service; 11 | 12 | import com.gcalsolaro.statemachine.domain.entity.Instance; 13 | import com.gcalsolaro.statemachine.domain.entity.User; 14 | import com.gcalsolaro.statemachine.domain.enums.InstanceState; 15 | import com.gcalsolaro.statemachine.domain.enums.UserState; 16 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationEvents; 17 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationStates; 18 | 19 | @Service 20 | public class SagaOrchestrator { 21 | 22 | @Autowired 23 | private StateMachine stateMachine; 24 | 25 | /** 26 | * Simple SAGA Orchestrator 27 | * 28 | * @param success 29 | * @return 30 | */ 31 | public StateMachine createInstanceSaga(boolean success) { 32 | this.setUpVariable(success); 33 | try { 34 | StateMachine sagaStateMachine = this.startSaga(); 35 | 36 | this.fireEvent(sagaStateMachine, ApplicationEvents.CREATE_INSTANCE); 37 | // 38 | // More check... 39 | // ... 40 | // More Stuff 41 | // 42 | this.fireEvent(sagaStateMachine, ApplicationEvents.CREATE_USER); 43 | 44 | ApplicationStates subcontextState = (ApplicationStates) sagaStateMachine.getExtendedState().getVariables().get("SUBCONTEXT_EVENT"); 45 | 46 | if (subcontextState.equals(ApplicationStates.END_USER_OK)) { 47 | this.fireEvent(sagaStateMachine, ApplicationEvents.UPDATE_INSTANCE_CREATED); 48 | } else if (subcontextState.equals(ApplicationStates.END_USER_KO)) { 49 | this.fireEvent(sagaStateMachine, ApplicationEvents.UPDATE_INSTANCE_REJECTED); 50 | } 51 | 52 | return sagaStateMachine; 53 | } finally { 54 | this.stopSaga(); 55 | } 56 | } 57 | 58 | @SuppressWarnings("unchecked") 59 | public List> getTransitions(StateMachine stateMachine) { 60 | List> transitions = new ArrayList<>(); 61 | 62 | for (Object objTrans : stateMachine.getTransitions()) { 63 | Transition transition = (Transition) objTrans; 64 | 65 | if (transition.getSource().getId().equals(stateMachine.getState().getId())) { 66 | transitions.add(transition); 67 | } 68 | } 69 | 70 | return transitions; 71 | } 72 | 73 | /** 74 | * Stop statemachine 75 | */ 76 | private void stopSaga() { 77 | stateMachine.stop(); 78 | } 79 | 80 | /** 81 | * Start statemachine 82 | * 83 | * @return 84 | */ 85 | private StateMachine startSaga() { 86 | stateMachine.start(); 87 | return stateMachine; 88 | } 89 | 90 | /** 91 | * Dispatch Event 92 | * 93 | * @param stateMachine 94 | * @param event 95 | * @return 96 | */ 97 | private boolean fireEvent(StateMachine stateMachine, ApplicationEvents event) { 98 | return stateMachine.sendEvent(event); 99 | } 100 | 101 | /** 102 | * Set variable for excecution 103 | * 104 | * @param success 105 | */ 106 | private void setUpVariable(boolean success) { 107 | stateMachine.getExtendedState().getVariables().put("success", success); // only for test 108 | stateMachine.getExtendedState().getVariables().put("instance", this.setUpInstance()); 109 | stateMachine.getExtendedState().getVariables().put("user", this.setUpUser()); 110 | } 111 | 112 | private Instance setUpInstance() { 113 | Instance i = new Instance(); 114 | i.setcInstance("SAGA_INSTANCE_TEST"); 115 | i.setInfo("test saga"); 116 | i.setState(InstanceState.NEW); 117 | return i; 118 | } 119 | 120 | private User setUpUser() { 121 | User u = new User(); 122 | u.setName("Giuseppe"); 123 | u.setSurname("Calsolaro"); 124 | u.setEmail("giuseppe.calsolaro@gmail.com"); 125 | u.setFiscalCode("CLSGPP87M28E409V"); 126 | u.setDtBorn(new Date()); 127 | u.setState(UserState.NEW); 128 | return u; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/config/StateMachineConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.statemachine.config.EnableStateMachine; 6 | import org.springframework.statemachine.config.StateMachineConfigurerAdapter; 7 | import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; 8 | import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; 9 | import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; 10 | 11 | import com.gcalsolaro.statemachine.domain.enums.InstanceState; 12 | import com.gcalsolaro.statemachine.domain.enums.UserState; 13 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationEvents; 14 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationStates; 15 | import com.gcalsolaro.statemachine.service.SagaService; 16 | 17 | @Configuration 18 | @EnableStateMachine 19 | public class StateMachineConfiguration extends StateMachineConfigurerAdapter { 20 | 21 | @Autowired 22 | private SagaService sagaService; 23 | 24 | @Override 25 | public void configure(StateMachineConfigurationConfigurer config) 26 | throws Exception { 27 | config 28 | .withConfiguration() 29 | .autoStartup(false) 30 | .listener(new StateMachineListener()); 31 | } 32 | 33 | /* 34 | * defines only entity's states 35 | */ 36 | @Override 37 | public void configure(StateMachineStateConfigurer states) throws Exception { 38 | states 39 | .withStates() 40 | .initial(ApplicationStates.START_INSTANCE) 41 | .state(ApplicationStates.START_INSTANCE) 42 | .state(ApplicationStates.INSTANCE_PENDING) 43 | .end(ApplicationStates.END_INSTANCE_OK) 44 | .end(ApplicationStates.END_INSTANCE_KO) 45 | .and() 46 | .withStates() 47 | .parent(ApplicationStates.INSTANCE_PENDING) 48 | .initial(ApplicationStates.START_USER) 49 | .stateEntry(ApplicationStates.EVALUATE_USER_PENDING, sagaService.evaluateUser()) // This action is performed automatically when the entity goes into the specified state. No explicit invocation is required 50 | .end(ApplicationStates.END_USER_OK) 51 | .end(ApplicationStates.END_USER_KO); 52 | } 53 | 54 | /** 55 | * defines the transactions and events that lead from one state to another 56 | */ 57 | @Override 58 | public void configure(StateMachineTransitionConfigurer transitions) throws Exception { 59 | transitions 60 | .withExternal() 61 | .source(ApplicationStates.START_INSTANCE) 62 | .target(ApplicationStates.INSTANCE_PENDING) 63 | .event(ApplicationEvents.CREATE_INSTANCE) 64 | .action(sagaService.createInstance()) // Action to be performed during the transition from one state to another 65 | .guard(sagaService.protectInstanceState(InstanceState.NEW)) // Guard interface is used to "protect" the transitions between states 66 | .and().withExternal() 67 | .source(ApplicationStates.START_USER) 68 | .target(ApplicationStates.EVALUATE_USER_PENDING) 69 | .event(ApplicationEvents.CREATE_USER) 70 | .action(sagaService.createUser()) 71 | .guard(sagaService.protectInstanceState(InstanceState.PENDING)) 72 | .guard(sagaService.protectUserState(UserState.NEW)) 73 | .and().withExternal() 74 | .source(ApplicationStates.EVALUATE_USER_PENDING) 75 | .target(ApplicationStates.END_USER_OK) 76 | .event(ApplicationEvents.UPDATE_USER_CREATED) 77 | .action(sagaService.saveUserState()) 78 | .and().withExternal() 79 | .source(ApplicationStates.EVALUATE_USER_PENDING) 80 | .target(ApplicationStates.END_USER_KO) 81 | .event(ApplicationEvents.UPDATE_USER_REJECTED) 82 | .action(sagaService.saveUserState()) 83 | .and().withExternal() 84 | .source(ApplicationStates.INSTANCE_PENDING) 85 | .target(ApplicationStates.END_INSTANCE_OK) 86 | .event(ApplicationEvents.UPDATE_INSTANCE_CREATED) 87 | .action(sagaService.updateInstance(true)) // the boolean is used for example purposes to manage the success or failure of the transaction 88 | .and().withExternal() 89 | .source(ApplicationStates.INSTANCE_PENDING) 90 | .target(ApplicationStates.END_INSTANCE_KO) 91 | .event(ApplicationEvents.UPDATE_INSTANCE_REJECTED) 92 | .action(sagaService.updateInstance(false)); 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /src/main/java/com/gcalsolaro/statemachine/service/SagaService.java: -------------------------------------------------------------------------------- 1 | package com.gcalsolaro.statemachine.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.statemachine.ExtendedState; 5 | import org.springframework.statemachine.StateContext; 6 | import org.springframework.statemachine.action.Action; 7 | import org.springframework.statemachine.guard.Guard; 8 | import org.springframework.stereotype.Service; 9 | 10 | import com.gcalsolaro.statemachine.domain.entity.Instance; 11 | import com.gcalsolaro.statemachine.domain.entity.User; 12 | import com.gcalsolaro.statemachine.domain.enums.InstanceState; 13 | import com.gcalsolaro.statemachine.domain.enums.UserState; 14 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationEvents; 15 | import com.gcalsolaro.statemachine.domain.enums.statemachine.ApplicationStates; 16 | import com.gcalsolaro.statemachine.repository.InstanceRepository; 17 | import com.gcalsolaro.statemachine.repository.UserRepository; 18 | 19 | @Service 20 | public class SagaService { 21 | 22 | @Autowired 23 | private InstanceRepository instanceRepository; 24 | 25 | @Autowired 26 | private UserRepository userRepository; 27 | 28 | public Action createInstance() { 29 | return new Action() { 30 | @Override 31 | public void execute(StateContext context) { 32 | Instance instance = findInstance(context.getExtendedState()); 33 | instance.setState(InstanceState.PENDING); 34 | instance = instanceRepository.save(instance); 35 | context.getExtendedState().getVariables().put("instance", instance); 36 | } 37 | }; 38 | } 39 | 40 | public Action createUser() { 41 | return new Action() { 42 | @Override 43 | public void execute(StateContext context) { 44 | User user = findUser(context.getExtendedState()); 45 | user.setState(UserState.CREATED); 46 | user = userRepository.save(user); 47 | // Save and update context variable 48 | context.getExtendedState().getVariables().put("user", user); 49 | } 50 | }; 51 | } 52 | 53 | public Action evaluateUser() { 54 | return new Action() { 55 | @Override 56 | public void execute(StateContext context) { 57 | User user = findUser(context.getExtendedState()); 58 | if (user != null) { 59 | if (user.getState().equals(UserState.CREATED)) { 60 | // the boolean is used for example purposes to manage the success or failure of the transaction 61 | boolean success = findSuccessState(context.getExtendedState()); 62 | if (success) 63 | context.getStateMachine().sendEvent(ApplicationEvents.UPDATE_USER_CREATED); 64 | else 65 | context.getStateMachine().sendEvent(ApplicationEvents.UPDATE_USER_REJECTED); 66 | } 67 | } 68 | } 69 | }; 70 | } 71 | 72 | public Action updateInstance(boolean success) { 73 | return new Action() { 74 | @Override 75 | public void execute(StateContext context) { 76 | Instance instance = findInstance(context.getExtendedState()); 77 | 78 | instance.setState(InstanceState.REJECTED); 79 | 80 | if (success) { 81 | User user = findUser(context.getExtendedState()); 82 | instance.setUser(user); 83 | instance.setState(InstanceState.CREATED); 84 | } 85 | 86 | instanceRepository.save(instance); 87 | } 88 | }; 89 | } 90 | 91 | /** 92 | * 93 | * @return 94 | */ 95 | public Action saveUserState() { 96 | return new Action() { 97 | @Override 98 | public void execute(StateContext context) { 99 | context.getExtendedState().getVariables().put("SUBCONTEXT_EVENT", context.getTarget().getId()); 100 | } 101 | }; 102 | } 103 | 104 | public Guard protectInstanceState(InstanceState state) { 105 | return new Guard() { 106 | @Override 107 | public boolean evaluate(StateContext context) { 108 | Instance instance = findInstance(context.getExtendedState()); 109 | if (instance != null) 110 | return instance.getState().equals(state); 111 | return false; 112 | } 113 | }; 114 | } 115 | 116 | public Guard protectUserState(UserState state) { 117 | return new Guard() { 118 | @Override 119 | public boolean evaluate(StateContext context) { 120 | User user = findUser(context.getExtendedState()); 121 | if (user != null) 122 | return user.getState().equals(state); 123 | return false; 124 | } 125 | }; 126 | } 127 | 128 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internal Helper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 129 | 130 | private Instance findInstance(ExtendedState extendedState) { 131 | for (Object obj : extendedState.getVariables().values()) { 132 | if (obj instanceof Instance) { 133 | return (Instance) obj; 134 | } 135 | } 136 | return null; 137 | } 138 | 139 | private User findUser(ExtendedState extendedState) { 140 | for (Object obj : extendedState.getVariables().values()) { 141 | if (obj instanceof User) { 142 | return (User) obj; 143 | } 144 | } 145 | return null; 146 | } 147 | 148 | private Boolean findSuccessState(ExtendedState extendedState) { 149 | for (Object obj : extendedState.getVariables().values()) { 150 | if (obj instanceof Boolean) { 151 | return (Boolean) obj; 152 | } 153 | } 154 | return null; 155 | } 156 | 157 | } 158 | --------------------------------------------------------------------------------