Search code examples
javaspringpostgresqlspring-bootjpa

Advice on handling transaction rollbacks with Spring Boot


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?


Solution

  • 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.