Transaction in Spring Boot as simple as possible

fabiothiroki

Fabio Hiroki

Posted on January 24, 2021

Transaction in Spring Boot as simple as possible

Introduction

A few days ago I had to investigate a bug in production that involved a database transaction, specifically defined by
@Transaction annotation. That sounded like a great opportunity to review the basic concepts and hopefully fix the bug.

No bug yet

Before the bug was introduced, this is how the application looked like: an endpoint that received some payload and then performed write on different tables in the same transactional annotation, using its respective dependencies:

@Autowired
private TableOneService tableOneService;

@Autowired
private TableTwoService tableTwoService;


@PostMapping("/")
@Transactional
public void save(@RequestBody Payload payload) {
  tableOneService.save(payload);
  tableTwoService.save(payload);
}
Enter fullscreen mode Exit fullscreen mode

So far, so good, the application was behaving as expected.

Requirements change

After observing the data persisted in table two, we've decided to change the business rule, validate the payload persisted based on its values. If you don't know the entire application and just focus on changing the tableTwoService code based on these specific requirements, this is a possible solution that you may consider:

  1. Add some king of validation on save method
  2. Throw an exception that will be translated to a Bad Request response to the client.

Don't feel guilty if you've considered this solution, we've also done this and deployed it to production.

This is what the code looked like now. As developers, we're so wrongly proud.

public class TableTwoService {
  public void save(Payload paylod) {
    if (isValid(payload)) {
     // persists on database
    } else {
     // throw some exception
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Bug detected

After some days, someone raised the hand:

"Hey something feels wrong, we're missing some data on table one since the deployment of the validation code".

Damn, we didn't even change the tableOneService neither the API controller code, for sure this is someone else's problem.

Database Transaction

In short words, this is what defines the behavior of a method annotated by @Transaction:

  1. Begin the transaction.
  2. Execute a set of data manipulations and/or queries.
  3. If no error occurs, then commit the transaction.
  4. If an error occurs, then roll back the transaction.
@Transactional
public void save(@RequestBody Payload payload) {
  // Both operations should work for transaction commit
  // Otherwise no operation will persist
  tableOneService.save(payload);
  tableTwoService.save(payload);
}
Enter fullscreen mode Exit fullscreen mode

Besides the code of tableOneService was executing without errors on runtime, the exception thrown on tableTwoService was rollbacking its persistence.

Let's fix the bug

The solution I've decided to use was to remove the exception thrown and instead just log the payload received so I could have better observability of this flow. An exception now won't trigger the transaction rollback.

In this case, my solution works because it's ok for the client that sent this request to not receive a Bad Request response whenever it sends an invalid payload, and just have its payload ignored.

public class TableTwoService {
  public void save(Payload paylod) {
    if (isValid(payload)) {
     // persists on database
    } else {
     // log a message and do nothing
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The end

The bug is now fixed and I've learned a little more about the @Transaction annotation. I hope this story helps someone else in the future.

💖 💪 🙅 🚩
fabiothiroki
Fabio Hiroki

Posted on January 24, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related