Use Case #02 — Spring transactional REQUIRES_NEW propagation mode
Hi ! Using REQUIRES_NEW propagation mode in Spring’s @Transactional annotation is rather rare and might be confusing. Because inner transaction uses an independent physical transaction, You should be careful and concerned about more negative scenarios, comparing to transactional flow with the default propagation mode: REQUIRED. For example:
- can inner transaction be committed, when outer transaction will rollback?
- can outer transaction be committed, when inner transaction will rollback?
- when both transactions be rolled back, and should they?
In this article I will describe common use cases with a brief summary — configuration and it’s result.
I am not going to describe how @Transactional annotation works and how Spring uses AOP proxies — there are many great articles describing this, and I will provide links to those articles at the end.
Code setup
To keep things simple I will include only necessary classes here. Let’s assume that we have some kind of Notification in our application
When notification is created, it has status NEW.
At some point in time, some kind of scheduler might decide that notification should be send somewhere. Imagine that because of business crazy requirement, status has to be updated in a one transaction, and transaction message should be updated in the other. For such case I have 2 services created:
Following service is going to be called first and start an outer transaction. It calls NotificationStatusUpdateService, where notification status will be updated in inner physical transaction. After that is updates notification’s message.
By default transactions are going to be marked for rollback only for runtime exceptions. To test rollbacks and be sure that it was triggered by me, I created a custom exception — it will be used in examples later.
Initial state
Before the service will be called, let’s assume that there is a notification with an id 1 present in the database, in the following state:

Test 1 — no exception thrown, everything committed
Service which creates outer transaction is called with notification id — custom exception is not being thrown anywhere.
notificationsService.sendNotification(1);
Result:

Explanation:
Outer transaction was suspended, because inner transaction has propagation mode set to REQUIRE_NEW. It finished successfully and was committed. After that outer transaction was resumed and committed. Because exception hasn’t been thrown, there was no reason to rollback — as a result both transactions have been committed.
Test 2 — exception thrown in an inner transaction, no handling in outer transaction
For this test I modified NotificationStatusUpdateService , so that inner transaction will throw NotificationStatusFailureException exception:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateNotificationStatus(Long id) {
Notification notification = notificationsRepository.findById(id).get();
notification.setStatus(Notification.NotificationStatus.SENT);
throw new NotificationStatusFailureException("Will it rollback?");
}
Let’s call the outer service as before:
notificationsService.sendNotification(1);
Result:

Explanation:
Outer transaction was suspended, because inner transaction has propagation mode set to REQUIRE_NEW. Inner transaction threw an exception and was rollbacked —notification status didn’t changed. Outer transaction was resumed, but it detected that an exception was thrown by inner transaction — because it hasn’t been handled , outer transaction was rollbacked too.
Test 3— exception thrown in an inner transaction, noRollbackFor=NotificationStatusFailureException in outer transaction
This example was a little bit confusing for me at the beginning. Inner transaction method looks the same as in test2, but @Transactional annotation for outer transaction has changed — now it looks like this:
@Transactional(noRollbackFor = NotificationStatusFailureException.class)
public void sendNotification(Long id) {
notificationStatusUpdateService.updateNotificationStatus(id);
Notification notification = notificationsRepository.findById(id).get();
notification.setMessage("UPDATED MESSAGE");
}
Let’s call the outer service as before:
notificationsService.sendNotification(1);
Result:

Explanation:
Outer transaction was suspended, because inner transaction has propagation mode set to REQUIRE_NEW. Inner transaction threw an exception and was rollbacked — notification status didn’t changed. Outer transaction was resumed, but it detected that an exception was thrown by inner transaction. Annotation parameter noRollbackFor=NotificationStatusFailureException on outer transaction was set to the same exception as the one thrown, but it would work, if outer transaction threw this exception. No update statements have been committed in the database.
Test 4— exception thrown in an inner transaction, try/catch block in outer transaction
Outer transaction method
@Transactional
public void sendNotification(Long id) {
try {
notificationStatusUpdateService.updateNotificationStatus(id);
} catch (NotificationStatusFailureException e) {
log.error("Inner transaction has thrown an exception", e);
}
Notification notification = notificationsRepository.findById(id).get();
notification.setMessage("UPDATED MESSAGE");
}
Let’s call the outer service as before:
notificationsService.sendNotification(1);
Result:

Explanation:
Explanation:
Outer transaction was suspended, because inner transaction has propagation mode set to REQUIRE_NEW. Inner transaction threw an exception and was rollbacked — notification status didn’t changed. Outer transaction was resumed, but it detected that an exception was thrown by inner transaction. Exception was caught and consumed in outer transaction, so that outer transaction could commit
Test 5— exception thrown in outer transaction
Inner transaction method
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateNotificationStatus(Long id) {
Notification notification = notificationsRepository.findById(id).get();
notification.setStatus(Notification.NotificationStatus.SENT);
}
Outer transaction method
@Transactional
public void sendNotification(Long id) {
notificationStatusUpdateService.updateNotificationStatus(id);
Notification notification = notificationsRepository.findById(id).get();
notification.setMessage("UPDATED MESSAGE");
throw new NotificationStatusFailureException("Outer rollback?");
}
Result:

Explanation:
Outer transaction was suspended, because inner transaction has propagation mode set to REQUIRE_NEW. Inner transaction committed successfully, and flow returned to previously suspended, outer transaction. Outer transaction throw an exception and was rollbacked independently of inner transaction.
Bonus 1 — monitor where Your transaction is being initialised and committed/rollbacked
You can simply change logging levels as following:
logging:
level:
org:
springframework:
orm:
jpa: trace
transaction:
interceptor: trace
to receive logs like:
Suspending current transaction, creating new transaction with name [pl.akolata.exercises.transactions.nested.NotificationStatusUpdateService.updateNotificationStatus]Opened new EntityManager [SessionImpl(809522138<open>)] for JPA transaction
...
Completing transaction for [pl.akolata.exercises.transactions.nested.NotificationStatusUpdateService.updateNotificationStatus]
...
Initiating transaction commit
Committing JPA transaction on EntityManager [SessionImpl(809522138<open>)]
Closing JPA EntityManager [SessionImpl(809522138<open>)] after transaction
...
Resuming suspended transaction after completion of inner transaction
You can make a separate profile like “transactions-logger” and use it during development. If it’s not enough information about transactions activity, You can set TRACE logging level for a whole org.springframework.transaction package.
Summary
Using REQUIRES_NEW propagation mode might be tricky, and can bring unexpected consequences, leaving Your database in a not consistent state.
References