The article is a part of JPA & Spring pitfalls series. Below you can find the actual list of all articles of the series:

Spring

JPA

Preface

In this article we will go through a few practical cases with code samples to show how tricky it may be to mix @Transactional propagation modes in Spring, especially REQUIRES_NEW, without being cautious enough.

Before we go, let’s only shortly remind the difference between REQUIRED and REQUIRES_NEW propagation modes.

REQUIRED

Support a current transaction, create a new one if none exists.
This is the default setting for @Transactional.

REQUIRES_NEW

Create a new transaction, and suspend the current transaction if one exists.

Practical, but tricky cases

Let’s imagine that we have a task to implement something as simple as just creating a new Person with a Wallet and persisting them in the database. Hence, we would like to have two entity classes with one-to-one relation. For each we should have Repository classes (we’re using Spring Data) and Service classes for operating on the entities. We should also have a few integration tests to check if it works.

Please take look at our initial code for this task. Important thing to notice is that in the PersonService we use @Transactional annotation (with default propagation mode – REQUIRED), whereas the WalletService has @Transactional(propagation = Propagation.REQUIRES_NEW).

Let’s see how it looks in the PersonService first:

This method creates a new Person object, persists it, passes it to the WalletService to do its job and then just simply returns the ID of the newly created person.

Let’s see what the WalletService does:

It creates and persists an empty Wallet entity, then sets reference to this wallet to the person object (passed to this method from the PersonService) and returns wallet instance.

Since the method is annotated with @Transactional, all the changes to managed entities are tracked and automatically saved to the database, so we don’t call anything like personRepository.save(person)to update DB entity, because we expect it to be done automatically at the end of createWalletAndAttachToPerson method.

Finally, we write an integration test to see whether this works as we expected.

We run this test and… it’s green – well done!

Code for this section is available on branch requires-new/initial.

It works fine, so let’s write another test

Since the PersonService is more complicated, writing a test for the WalletService is just a formality.

So we run the test and… wait… it failed – margaretWallet was in fact null.

This means that person.setWallet()wasn’t propagated to the database at all this time, but just a minute ago it worked fine in PersonServiceTest. Why?

The thing is, that indeed any changes to entities done in @Transactionalmethods are automatically reflected in the database at the end of transaction, but it only applies to the entities saved, merged or retrieved within this specific transaction.

In our case, createWalletAndAttachToPerson method starts a completely new transaction, therefore it won’t save changes made to the person instance that was passed from a different transaction context.

PersonServiceTest succeeds, because it calls PersonService.createPerson(), which is a transactional method where person was persisted, therefore person in managed entity state within that transaction. This means that at the end of createPerson method, the current state of the person will be populated to the database. That’s why the test passed (even though createWalletAndAttachToPerson didn’t work as initially expected) and gave us a false impression that everything is alright.

Code for this section is available on branch requires-new/wallet-service-test.

Remember letter “I” of ACID?

Now, since we know that passing an entity between different transaction contexts is usually a bad idea, let’s refactor createWalletAndAttachToPerson to accept not a Person object, but just personId. This way we can retrieve the person entity by ID within this transaction to have it in managed entity state, which means that its state will be reflected in the database with transaction’s end. So, finally we come up with something like this:

It looks good, so we run tests and see that the WalletServiceTest passes now, but the PersonServiceTest now fails due to an exception thrown in line personRepository.findById(personId).orElseThrow().

How is that possible if we have just saved the Person entity in createPerson() method right before calling the WalletService?

In WalletServiceTest the entity is saved out of transactional context, therefore it’s persisted right away and it’s visible to the WalletService. Whereas, PersonService.createPerson() is executed in a transactional scope and here transaction isolation comes into play. Normally, any changes (save/update/delete) are visible only within the same transaction. The world outside of this transaction won’t see them until the transaction is successfully committed.

When you use REQUIRES_NEW propagation from an existing transaction context, the existing transaction is suspended and the new transaction starts. Once that new transaction ends, its changes are committed and visible to the the original transaction, which gets resumed them. However, it doesn’t work the other way. The “new transaction” cannot see changes made by the original one, because it’s suspended and has not commited them yet. That’s why findById(personId)failed to find the Person entity that was created in a different transaction and not yet committed.

Code for this section is available on branch requires-new/isolation.

Detached or managed?

Let’s make another change. If in the WalletService we can’t see a person saved in the primary transaction, let’s try the other way – WalletService will only create a new Wallet in a separate transaction:

then in PersonService.createPerson() we’ll update its money amount and set reference to it to the person object:

As you probably suppose, again, this won’t work. In fact, now we can see changes done by createWallet, because this transaction has finished with success. However, we can’t expect that changes to the returned wallet entity (created in a different transaction) will be automatically recorded in database, because for createPerson, wallet isn’t in managed state, but detached. So to make the code working, we would need to for instance add walletRepository.save(wallet)after altering wallet or instead of using the returned entity we would need to retrieve “a fresh one” from the database in the current transaction.

As a rule, remember that when you pass entities to a method withREQUIRES_NEWor when you return entities from it – they are in detached state and this may mislead other developers. Therefore, on the boundaries of transaction contexts, it’s always better to operate on entities IDs instead of passing entities themselves.

Code for this section is available on branch requires-new/detached.

Rollback

Alright, now our manager told us a new requirement, namely that we shouldn’t allow creating a person with a negative initial amount of money. In that case we should throw an exception and nothing should be inserted into database.

Instead of doing it programmatically, we can leverage transactions and throw a runtime exception to trigger a rollback. So here’s our code:

and a new test case:

What we face another time is that our test fails. Rollback mechanism reverts changes in the entities that are tracked by the current transaction. Creating a wallet happens in a different transaction than throwing the exception, therefore it won’t be rolled back.

 

Finally, we’re getting to the point that @Transactional(propagation = Propagation.REQUIRES_NEW)does more harm than good in our case. Therefore, we remove that, run each test case separately one by one and… it finally works.

Code for this section is available on branch requires-new/rollback.

Beware of transactional tests

It looks so great to see tests light up in green… So let’s rerun them altogether, not one by one. Wait… one of them actually failed and for the first time today it’s not about REQUIRES_NEW propagation.

The PersonServiceTest contains two test cases, which are not independent, because the first one adds a person to the database whereas the second expects this table to be empty. So we need to add a mechanism to revert database to the initial state after each test case.

Reading Spring docs (source, source) you may find an interesting trick. With @Transactionalannotation on test method or test class level, Spring by default rolls back such transactions after the test, what prevents committing any changes to the database.

It’s nice, clean and it works for our case. But? But it may end up in false positive test results.

Remember when we discovered a bug in the WalletService (“It works fine, so let’s write another test” section) thanks to the test case which didn’t go through a transactional context? Now, imagine that you add @Transactionalto make test cases independent. What happens? The failing test case now passes, not because it fixed the problem, but because it hid it! Even adding entityManager.flush(), which is advised by Spring docs to avoid false positives, doesn’t help in that case.

If you want to test how @Transactionalinfluences behaviour of the WalletServiceTest by yourself, check out branch requires-new/transactional-test.

If you are interested in reading more about this pitfall, I recommend Tomasz Nurkiewicz’s article.

Conclusions

Spring’s declarative transaction management is really cool and easy-to-use feature, but at the same time it may be the cause of hard to detect bugs, so remember to be cautious when using @Transactional, especially when you are about to use REQUIRES_NEW propagation mode.

A few basic things to remember from this article:

  • Prefer returning and passing IDs (or DTOs) over entity objects on the boundaries of a transaction
  • Entity modifications are tracked only within a transaction in which they have been saved, merged or retrieved
  • Rollbacks revert only changes to the entities tracked by the current transaction
  • Avoid transactional tests, until you specifically want to test how code behaves in a transactional context

Thanks for reading and stay tuned!

Java Developer

Michał is an eager fan of the fresh approach to Java programming. Enthusiast of Spring tech stack and refactoring techniques. Enjoys solving Java quirks, algorithmic puzzles and... Rubik's cube. Privately, amateur drummer.