Search code examples
javajpaspring-data-jpa

JPA Stop Executing Code in Transaction if Exception Occurs


I have a JPA transaction like the following (Using controller advice to catch exceptions)

@Transactional
public void save(MyObj myObj) {
  // Attempt to save the object
  this.myRepo.save(myObj)
  // After it saves, call my audit log service to record the change
  this.myAuditLogService.logChange(myObj)
}

Works fine, but the problem is if the save fails and throws an exception, it still calls the audit log service, and then throws an exception afterwards. Causing erroneous audit entries to be created.

Expected Flow

  1. Call save function
  2. Save fails
  3. Transaction stops and rolls back
  4. Controller advice catches the exception

Actual Flow

  1. Call save function
  2. Save fails
  3. Audit log service is called
  4. Transaction rolls back
  5. Controller advice catches the exception

Solution

  • This is a common problem in Computer Science in Distributed Systems.

    Basically what you want to achieve is to have atomic operation across multiple systems.

    Your transaction spans only your local (or first) database and that's all. When the REST call to the second system is initiated and successful but the first save results in crash you want to have rollback on the first system (first save) and rollback on the second system as well. There are multiple problems with that and it's really hard to have atomic-like consistency across multiple systems.

    1. You could use Database supported technologies for such cases:

    What you probably need is a 2PC / 3PC or change the processing of your request somehow. The trade-off of course will be that you'll have to sacrifice immediate results to have eventual consistency.

    1. You could use eventual-consistency

    For example send message to some storage for later processing -> make both systems read the message:

    • System1 reads from storage this message and will save myObj
    • System2 reads from storage this message and will log change

    This will of course happen "eventually" - there will never be a guarantee that either system is up at the time of the processing or even later on (e.g. somebody killed the server or deployed code with bug and the server restarts indefinitely).

    Moreover you'll sacrifice read-after-write consistency.

    You could use in case of a failure a Compensating transaction.

    I recommend reading more on the topic of Distributed Systems in: