There is a trend in current backend applications to change the flow of service invocations to event-based ones. I assumed it was due to the growing influence of Domain Driven Design concepts. Picking this approach is sometimes justified, sometimes it is just “hype”. I’d like to focus on the consequences of introducing events in the spring-based applications you may encounter in conjunction with database transactions.

Let’s examine a simple spring boot application you may find here. It contains one rest API endpoint which registers users. Registering consists of two actions:

save user into database using UserRepository interface

publish event UserCreated in the application

This actions happen in one transactional method in UserService which is called from UserResource (spring controller). So far, nothing fancy.

Let’s extend our application to handle the UserCreated event.

I wrote a listener (UserQueueNotifier) whose responsibility is quite simple:

subscribe to an event

load user from the database based on data (userId) from the event (see the disclaimer )

) put email and generated registration token to jms queue to simulate communication with the external system responsible for sending the registration confirmation email

Disclaimer: Some of you recognized, at a glance, that such an event, with just part of the data (userId) is against the rules of DDD. In a pure DDD world, we should include the user inside the UserCreated event. I did it on purpose for two reasons — one is that it simplifies the example and second, maybe more important, is that I encounter such a solution in a real project which does not follow all of the DDD rules.

To subscribe to spring events, you just need to write a void method which takes the event as a parameter and marks it as @EventListener. To prove that everything works together you can ran tests from class UserResourceHbTest (tests should be self-explanatory). One of them checks if after registration, the proper message is present on queue.

Listener (UserQueueNotifier) comes with the special implementation of UserRepository interface which underneath uses the spring data framework. I called this implementation HbUserRepository.

Let’s imagine that for some reason, you decided to switch the implementation of UserRepository interface and use a native sql. I called it JdbcTemplateUserRepository — it uses JdbcTemplate for querying.

After this “infrastructural” change, I ran the tests (try UserResourceJdbcTemplateTest) and, here comes the tricky part, the tests are red! I hope you are surprised. Everything is due to transaction propagation in spring.

Transactions boundaries are a clue

We need to start our explanation from the top and see where the transaction starts. UserService is marked as @Transactional so everything inside method createUser, if runs in the same thread, is in the same transaction. After saving our user through the repository, we publish an event. By default, publishing an event is synchronous, so you can imagine as if all the method body marked as @EventListener is inlined into createUser method. Underneath publishEvent method spring is just looping over all the listeners’ methods and simply calls them. No magic, no hidden asynchronous calls.

If code from the listener runs in the same thread from where it was called, it propagates the same transaction by default. It means that when we call find method on UserRepository in UserQueueNotifier listener we are still in a not committed transaction. Commit will happen after jumping out of the method marked as @Transactional so after leaving the listener. How, then, is it possible for us to read the user from our database?

That is the tricky part — if UserRepository uses underneath implementation provided by spring data, then the user can be found because it is stored in a hibernate session. We do not touch the database at all, we simply retrieve the user from the session.

If we changed the implementation to query using native sql through JdbcTemplate then of course the user would not be found in database because transaction wouldn’t have been committed yet! That is why we got a red test.

Commit transactions when entering listener

Can we handle such a common situation with spring mechanisms? The answer is of course, yes.

You need to use @TransactionalEventListener annotation over method mark as @EventListener. By default, it has set attribute phase to TransactionPhase.AFTER_COMMIT which means the listener code will be executed when the transaction from which it was called will be committed.

Is the transaction propagated after commit?

The question is: are we still in transaction when our listener code is executed after commit or do we need to open a new one? What happens if we write operations in listener?

We need to read the documentation about transaction synchronization from the spring documentation. It says “Invoked after transaction commit. Can perform further operations right after the main transaction has successfully committed.” But the most important part comes below that. “The transaction will have been committed already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still “participate” in the original transaction, allowing it to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction”.

So we will be in a transaction but the commit will not be done!? That’s strange but the documentation comes with a solution. We need to declare that this part of code will run in a new transaction. It can be achieved in two ways:

we can use as the documentation says @Transactional(propagation = Propagation.REQUIRES_NEW) which simply starts the new transaction

we can use standard @Transactional but since the transaction is propagated within the same thread, we need to run a new one. We can use @Async annotation on our method

In my simple example, I decided to use the same thread and start a new transaction there. Choosing the second approach requires rewriting tests that are able to wait for the completion of this new thread.

Try it yourself

To demonstrate the problem with commit in listener, please remove @Ignore on registeredUserHasTokenGenerated method in UserResourceTest. The test should fail until you add @Transactional(propagation = Propagation.REQUIRES_NEW) on listener method, because write to database (userRepository.save) is not committed.

Summary

If you are going to write code using spring transactions and event publishers, you need to remember a handful of rules:

the transaction is bound to a thread

by default, the transaction is committed when you jump out the method marked as @Transactional

by default, all methods called inside the transaction use it

by default, event listeners are executed in the caller thread which means they use the same transaction as caller

to ensure that your method is invoked after the transaction of the caller is committed, you need to use @TransactionalEventListener

if you use @TransactionalEventListener then remember that you need to start a new transaction to commit any write operations happening there

If you remember these rules, nothing with spring transactions and events publishing should surprise you.