User State Management Using Spring Statemachine

Veer Pal
4 min readJun 21, 2024

--

A state machine in Spring Boot typically involves the use of the Spring State Machine project, which provides infrastructure for defining and executing state machine workflows. Here’s a step-by-step guide on how state machines work in Spring Boot:

State

A State represents a particular status or condition of an entity at a specific point in time. In a state machine, states are the nodes or points that signify different phases or conditions an entity can be in.

Event

An Event is a trigger that causes a state transition. Events are the actions or occurrences that can change the state of the entity.

Guard

A Guard is a condition that must be met for a transition to occur. Guards are used to control whether a state transition should be allowed based on certain criteria or logic.

Action

An Action is a task or operation that is executed during a state transition. Actions can perform various functions such as updating records, sending notifications, or logging activities

For example, an user management platform we want to change user status based on some conditions .

Add maven dependency

Use below maven dependency to use spring statemachine

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

Add all user status in table user_status so that we can extend it in future .

Create table user_status_event and store events with dynamics guard .

This code defines a configuration class UserStatusStateMachineConfig for a state machine using Spring State Machine.

@Configuration
@EnableStateMachine
public class UserStatusStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {

@Autowired
private UserStatusRepository userStatusRepository;

@Autowired
private UserStatusEventRepository userStatusEventRepository;
@Autowired
private UserStatusEventAction userStatusEventAction;
@Autowired
private ApplicationContext applicationContext;

@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
List<UserStatus> userStatuses = userStatusRepository.findAll();
StateConfigurer<String, String> stateConfigurer = states.withStates();
if (!userStatuses.isEmpty()) {
stateConfigurer.initial (userStatuses.get(0).getStatusCode());
}
for (UserStatus userStatus : userStatuses) {
stateConfigurer.state(userStatus.getStatusCode());
}
}


@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
List<UserStatusEvent> eventEntities = userStatusEventRepository.findAll();

for (UserStatusEvent eventEntity : eventEntities) {
if (StringUtils.isNotBlank(eventEntity.getEventGuard())) {
transitions.withExternal()
.source(eventEntity.getSourceStatus())
.target(eventEntity.getTargetStatus())
.event(eventEntity.getEventType())
.guard((Guard<String, String>) applicationContext.getBean(eventEntity.getEventGuard()))
.action(userStatusEventAction);
} else {
transitions.withExternal()
.source(eventEntity.getSourceStatus())
.target(eventEntity.getTargetStatus())
.event(eventEntity.getEventType())
.action(userStatusEventAction);
}
}
}
}

Create Guard RegistrationGuard class that implements the Guard interface. The purpose of this class is to evaluate a condition based on the state of a system and return a boolean result

@Component
public class RegistrationGuard implements Guard<String,String> {
@Autowired
private UserProfileRepository userProfileRepository;
@Autowired
private SystemPreferencesUPSRepository systemPreferencesUPSRepository;

@Override
public boolean evaluate(StateContext stateContext) {
String workspace = (String) stateContext.getExtendedState().getVariables().get("workspaceId");
Optional<SystemPreferencesUPS> systemPreferencesOptional= systemPreferencesUPSRepository.findById("ACTIVATION_PENDING_ALLOW_FOR_"+workspace);
if(systemPreferencesOptional.isPresent()){
SystemPreferencesUPS systemPreferencesUPS= systemPreferencesOptional.get();
return "Y".equals(systemPreferencesUPS.getDefaultValue());
}

return false;
}
}

This code defines a UserStatusEventAction class, which implements the Action interface from the Spring State Machine framework. It handles actions that occur during state transitions in a state machine.


@Component
@Transactional
public class UserStatusEventAction implements Action<String, String> {

@Autowired
private UserProfileRepository userProfileRepository;
@Autowired
private IdGeneratorUPS idGenerator;
@Autowired
private UserStatusChangeHistoryService userStatusChangeHistoryService;

@Override
public void execute(StateContext<String, String> context) {
String userId = (String) context.getExtendedState().getVariables().get("userId");
String newState = context.getTarget().getId();
UserProfile userProfile = userProfileRepository.findById(userId)
.orElseThrow(() -> new ApplicationException(ErrorCodes.USER_NOT_FOUND));
userProfile.setPreviousStatus(userProfile.getStatus());
userProfile.setStatus(newState);
userProfile.setStatusChangeOn(new Date(System.currentTimeMillis()));
userProfileRepository.save(userProfile);
storeStatusChangeHistory(userProfile);
}

private void storeStatusChangeHistory(UserProfile userProfile){
UserStatusChangeHistory userStatusChangeHistory= new UserStatusChangeHistory();
userStatusChangeHistory.setUserStatusChangeHistoryId(idGenerator.generateTransactionId(Constants.STATUS_CHANGE_PREFIX.getStrValue()));
userStatusChangeHistory.setUserId(userProfile.getUserId());
userStatusChangeHistory.setPreviousStatus(userProfile.getPreviousStatus());
userStatusChangeHistory.setChangeStatus(userProfile.getStatus());
userStatusChangeHistory.setChangeOn(userProfile.getStatusChangeOn());
userStatusChangeHistoryService.save(userStatusChangeHistory);

}
}

Sending the event to change states in the state machine


@Service
public class UserStatusEventServiceImpl implements UserStatusEventService {

@Autowired
StateMachine<String, String> stateMachine;
@Autowired
UserProfileRepository userProfileRepository;

@Override
public void onRegistration(String userId,String workspaceId) {
stateMachine.getExtendedState().getVariables().put("userId", userId);
stateMachine.getExtendedState().getVariables().put("workspaceId", userId);
stateMachine.sendEvent("onRegistration");
}

@Override
public void onUserSuspend(String userId) {
StateMachine<String,String> stateMachine= build(userId);
stateMachine.getExtendedState().getVariables().put("userId", userId);
stateMachine.sendEvent("onSuspention");
}


private StateMachine<String, String> build(String userId)
{
UserProfile userProfile = userProfileRepository.findById(userId)
.orElseThrow(() -> new ApplicationException(ErrorCodes.USER_NOT_FOUND));

stateMachine.stop();
stateMachine.getStateMachineAccessor().doWithAllRegions(accessor -> {
accessor.resetStateMachine(new DefaultStateMachineContext<>(
userProfile.getStatus(), null, null, null)
);
});

stateMachine.start();

return stateMachine;
}

}

Conclusions for State Machine in User Status Management

The implementation of the state machine for user status management in this system provides a robust, scalable, and maintainable solution for handling complex state transitions. By leveraging Spring State Machine, the system benefits from a clear structure, dynamic state management, transactional integrity, and extensibility. This approach not only simplifies the handling of user status changes but also ensures that the system can grow and adapt to new requirements with ease.

--

--