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