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);
}
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:
- Add some king of validation on
save
method - 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
}
}
}
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
:
- Begin the transaction.
- Execute a set of data manipulations and/or queries.
- If no error occurs, then commit the transaction.
- 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);
}
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
}
}
}
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.
Top comments (4)
Are you aware of using the
spring.jpa.open-in-view
property which is by defaulttrue
? This creates an transaction over each rest call...more details here:
stackoverflow.com/questions/305494...
I didn't know about this property! Thanks for the information!
Just curious, are table 1 and 2 services always used together like this?
No, not always, some other endpoint uses persists only in table 2.