Creating UserDetailExecutor

In the OrderController class The UserDetailExecutor has been mentioned as the initial executor. As per the diagram, you know that executor should be a Query-Executor. Because the requirement is only fetching the user details. Fetching user details doesn’t change any data.

(1)
@SagaExecutor(executeFor = "user-service", liveCheck = false, value = "UserDetailExecutor")
@RequiredArgsConstructor
public class UserDetailExecutor implements QueryExecutor<PlaceOrderAggregator> { (2)

    (3)
    private final UserService userService;

    @Override
    public ProcessStepManager<PlaceOrderAggregator> doProcess(
            PlaceOrderAggregator currentAggregator, (4)
            ProcessStepManagerUtil<PlaceOrderAggregator> stepManager (5)
    ) throws
    RetryableExecutorException, NonRetryableExecutorException (6)
    {
        currentAggregator.getExecutions().add(this.getClass().getSimpleName()+":"+ LocalDateTime.now());
        (7)
        UserDetailDto.ResponseBody userDetails = this.userService.getUserDetails(currentAggregator.getUserId());
        (8)
        currentAggregator.setDeliveryDetails(userDetails.getDeliveryDetails());
        return stepManager.next(OrderInitializeExecutor.class, OrderStatus.USER_DETAILS_FETCHED); (9)
    }
}
1 Annotate the UserDetailExecutor class as a SagaExecutor by using @SagaExecutor annotation.

Read more about @SagaExecutor annotation.

2 Implement the UserDetailExecutor from the QueryExecutor by passing the target Aggregator.

Read more about QueryExecutor<A> interface.

3 Autowire the UserService to make the execution by calling the user-service.
4 you will have the currentAggregator object through the method parameter. due to this executor is the 1st executor, the currentAggregator object contains the initialized values from the OrderController class.
5 stepManager helps you to navigate the execution to the next executor.
6 If you throw an exception, that is how the framework knows that the executor has an exception at this moment. Based on the exception type that you have thrown, The framework decides that the process should be started the revert process or either process should be stopped temporally.

IF the exception is a RetryableExecutorException the framework stops the process temporally. Or if the exception is a NonRetryableExecutorException the framework knows that the execution cannot be processed to forward anymore and the revert process should be started. Throwing an exception can be implemented in the method as well. But in this example, we categorize the exceptions whether the exception is a RetryableExecutorException or otherwise a NonRetryableExecutorException in the UserService (service layer).

7 By using the UserService’s getUserDetails() method, fetches the user’s data from user user-service.
8 After receiving the user’s detail the currentAggregator is updated with user’s details. Therefore, you can access the user’s detail within the next executor.
9 By using the stepManager, navigates the execution to the next executor called and the event name marked as USER_DETAILS_FETCHED OrderInitializeExecutor.

Creating UserService.

Userservice is a regular spring service that creates for calling user-service. Due to the user-service is another utility service we have to call it through http or any other protocol that target service supports (Most probably API endpoints are exposed by http protocol). This example’s user-service also support http endpoint to access the API. Therefore, we will be using rest template as http client. But you can use any Http client like Feign-client, Okhttp, Spring-webclient and so on.

@Service
@RequiredArgsConstructor
public class UserService {

    private final RestTemplate restTemplate;

    public UserDetailDto.ResponseBody getUserDetails(String userId)
            throws
            NonRetryableExecutorException,  RetryableExecutorException (1)
    {

        try {
            (2)
            UserDetailDto.ResponseBody responseBody = this.restTemplate.getForObject(
                    "http://user-service/users/{userId}",
                    UserDetailDto.ResponseBody.class,
                    userId
            );
            assert responseBody != null;
            return responseBody;
        } catch (HttpClientErrorException ex) { (3)
            // This exception is thrown for HTTP 4xx errors (Client errors)
            // You can handle specific HTTP error codes here
            if (ex.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
                (4)
                throw NonRetryableExecutorException
                        .buildWith(
                                new RuntimeException("User not found"),
                                ""
                        )
                        .put("reason", "User not found")
                        .build();
            } else {
                (5)
                throw NonRetryableExecutorException.buildWith(ex, "").build();
            }
        } catch (HttpServerErrorException ex) { (6)
            // This exception is thrown for HTTP 5xx errors (Server errors)
            // You can handle specific HTTP error codes here
            if (ex.getStatusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
                (7)
                throw NonRetryableExecutorException.buildWith(ex, "").build();
            } else {
                (8)
                //502 , 503, 504, 509 etc.
                throw RetryableExecutorException.buildWith(ex).build();
            }
        } catch (RestClientException ex) { (9)
            // This exception is a generic RestClientException
            // Handle other types of exceptions here
            throw ex; (10)
        } catch (IllegalArgumentException illegalArgumentException) { (11)
            throw RetryableExecutorException.buildWith(illegalArgumentException).build();
        } catch (RunT) { (11)
            throw RetryableExecutorException.buildWith(illegalArgumentException).build();
        }
    }
}
Handling the exceptions is the most important task in StackSaga framework. You have to handle the exception, and decide what should be the NonRetryableExecutorException and what should be the RetryableExecutorException carefully. For example, in this demo you saw that we handled NOT_FOUND status. the NOT_FOUND can be thrown if the endpoint not found that you are looking for. Or otherwise it can be passed if the user does not exist. Then, if you have not any awareness about what the target service returns, you will not be able to catch the real error. In this example, we know exactly there is an endpoint /users/{userId} in the user-service therefore no worries. But be careful if you access third party APIs. Read the API documentation in detail.
1 We have thrown both NonRetryableExecutorException,and RetryableExecutorException that UserDetailExecutor’s doPrcess() method expects. That’s why it was mentioned in above. The handling exception part is done in the service layer.

[ Read the TIP ]

2 Call the http request to the user-service.
3 Catch the 4xx HTTP errors to determine if the exception is a NonRetryableExecutorException or RetryableExecutorException.
4 Due to the http error code is equal to NOT_FOUND (404), the process cannot be done anymore. Therefore, a NonRetryableExecutorException is thrown by wrapping with the real exception. If you want to put some data based on the exception, you can use the put("key","value") method for that. The data can be accessed from any revert-exceptions.
5 Other 4xx errors are thrown as the NonRetryableExecutorException by wrapping the real error.
6 Catch the 5xx HTTP errors to determine if the exception is a NonRetryableExecutorException or RetryableExecutorException. Most probably 5xx errors can be retried, but there are some cases it can not.
7 Check the 5xx error is equal to INTERNAL_SERVER_ERROR. Because if there is an internal server in this case, we know that we cannot go ahead and the process should be stopped going forward. Therefore, NonRetryableExecutorException is thrown by wrapping the real letter.
8 If the 5xx is not equal to INTERNAL_SERVER_ERROR, then other errors like 502, 503, 504, 509 error codes are caught as RetryableExecutorException and therefore a RetryableExecutorException is thrown by wrapping the real exception.
9 Cathe the other exceptions.
10 In this example, that other error codes are not considered because we assume that errors cannot be happened. Therefore, that error just throws without wrapping with NonRetryableExecutorException. IF you want to wrap, you can do as usual but is not required if you don’t consider those errors. Because internally the framework wraps the all RuntimeExceptions with NonRetryableExecutorException by default.
11 Due to we are using spring-cloud-load-balancer, when we make a request via the RestTemplate internally load balancer check is there any registered services in the local cache. Then, if there is no instance in the cache, it throws and exception with IllegalArgumentException. But in our case, actuality it is also a retryable exception. Because when an instance is registered, that execution can be invoked. Therefore, that error is thrown as RetryableExecutorException.
The reason for handling the exception is that this is where the http client does the invocation and the special this is most probably the exceptions are different to each other even though the http status code is the same.
Case-1

IF you change the Rest-Client (For instance, you move to RestTemplate to Feign-client), all the exceptions are changed. Then you have to change all the codes in the executor if you have handled the exceptions inside the executor. But in this way nothing to do anything.

Case-2

If you have to change the protocol like HttpRest to GRPC, you have nothing to do in the executor layer.