skip to Main Content

I have a Spring Boot application that relies on 2 data sources: an third-party HTTP API and a PostgresSQL db. The database is queried using Spring Data JPA, I have set up all the classes for the entities and corresponding repositories.
There is a service with a transactional method that looks something like this:

@Service
public class MyService {
 
    @Autowired
    private EntityRepository entityRepository;
    
    @Autowired
    private ApiService apiService;

    @Transactional
    public Entity createEntity(EntityInputData data) {
        this.apiService.createEntity(data); // HTTP call
        Entity entity = new Entity(data);
        return this.entityRepository.save(entity);
    }
}

From what I understand, if any exception were to be thrown during the execution of createEntity, the Entity object would not be persisted in the database thanks to the @Transactional annotation. However, there is nothing preventing or reverting the API call if the entity ends up not being created.

I tried adding a try/catch within createEntity, but I noticed that exceptions thrown while committing the transaction would not be catched (ie because of a PostgresSQL error), since they are thrown after the method has actually been executed:

    @Transactional
    public Entity createEntity(EntityInputData data) {
        string apiId = this.apiService.createEntity(entity); // HTTP call
        try {
            Entity entity = new Entity();
            return this.entityRepository.save(entity);
        } catch (Exception e) {
            // Not called if exception is thrown while committing transaction
            this.apiService.deleteEntity(apiId);
            throw e;
        }
    }

If I move the try/catch outside around the call to the method, then I can catch these exception. The problem is that I have no context information for handling the rollback in the third-party API.

try {
    myService.createEntity(data);
} catch (Exception e) {
    // How do I call my API to tell it to rollback whatever has been created?
}

I could not find a method to reflect a DB rollback to an external service that matches my use case, any help?

2

Answers


  1. Chosen as BEST ANSWER

    I found this solution that seems to work ok using a TransactionalEventListener. Simply publish an event after calling the API service, then catch the event only if the transaction is aborted. Since I can pass whatever data I need to the event, I can easily call my service again to revert the changes:

    MyService:

    @Service
    public class MyService {
     
        @Autowired
        private EntityRepository entityRepository;
        
        @Autowired
        private ApiService apiService;
    
        @Transactional
        public Entity createEntity(EntityInputData data) {
            this.apiService.createEntity(data); // HTTP call
            Entity entity = new Entity(data);
            return this.entityRepository.save(entity);
        }
    }
    

    ApiService:

    @Service
    public class ApiService {
    
        @Autowired
        private ApplicationEventPublisher eventPublisher;
    
        public void createEntity(EntityInputData data) {
            // Do HTTP call
            ApiEvent event = new ApiEvent(/* Whatever data was returned by the API */);
            this.eventPublisher.publishEvent(event);
        }
    
        public void deleteEntity(data) {
            // Do HTTP call
        }
    }
    

    Then declare a Component for handling events:

    @Component
    public class ApiEventHandler {
    
        @Autowired
        private ApiService apiService;
    
        @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
        void onCreateFailed(ApiEvent event) {
            apiService.deleteEntity(event.data)
        }
    }
    

    The idea is that the event will only be treated if there is a rollback. Plus I no longer have to deal with a try/catch, everything works as if the method in my ApiService was transactional as well.


  2. Search on: saga design pattern

    In this case, saga orchestration seems the simplest approach. Basic idea is to split MyService into separate classes, something like

    // package private 
    @Component
    class LocalEntityHandler {
    
        @Transactional
        public Entity createEntity(EntityInputData data) {
            Entity entity = new Entity();
            return this.entityRepository.save(entity);
            }
        }
    }
    

    and

    // package private 
    @Component
    class RemoteEntityHandler {
    
        public String createEntity(EntityInputData data) {
            return this.apiService.createEntity(entity);
        }
    
        public void deleteEntity(String apiId) {
            return this.apiService.createEntity(entity);
        }
    }
    

    then MyService becomes an orchestrator for each step of the process:

    @Service
    public class MyService {
        private final LocalEntityHandler local;
        private final RemoteEntityHandler remote;
       // constructor omitted
    
        public Entity createEntity(EntityInputData data) {
            var apiId = remote.createEntity(data);  // track the context
            try {
                return local.createEntity(data);
            } catch (Exception e) {
                remote.delete(apiId);  // use the tracked context to clean up
                throw e;
            }
        }
    }
    

    and calls the remote API to delete the entity on an exception.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search