Spring Transaction Propagation in a Nutshell
Diving into Spring Transaction Propagation mechanism
Join the DZone community and get the full member experience.
Join For FreeSpring allows you to control the behavior of logical and physical transactions via transaction propagation mechanisms. There are seven types of transaction propagation mechanisms that you can set in a Spring application via org.springframework.transaction.annotation.Propagation
.
By default, the only exceptions that cause a transaction to roll back are the unchecked exceptions (like RuntimeException
). Nevertheless, you can control this aspect via the noRollbackFor
, noRollbackForClassName
, rollbackFor
, and rollbackForClassName
elements of @Transactional
.
Propagation.REQUIRED
Propagation.REQUIRED
is the default setting of a @Transactional
annotation. The REQUIRED
propagation can be interpreted as follows:
- If there is no existing physical transaction, then the Spring container will create one.
- If there is an existing physical transaction, then the methods annotated with
REQUIRE
will participate in this physical transaction. - Each method annotated with
REQUIRED
demarcates a logical transaction and these logical transactions participate in the same physical transaction. - Each logical transaction has its own scope, but, in case of this propagation mechanism, all these scopes are mapped to the same physical transaction.
Because all the scopes of the logical transactions are mapped to the same physical transaction, when one of these logical transactions is rolled back, all the logical transactions of the current physical transaction are rolled back.
Consider the following two logical transactions (or think of it as one outer logical transaction containing an inner logical transaction):
propagation=Propagation.REQUIRED) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
insertSecondAuthorService.insertSecondAuthor();
}
x
propagation = Propagation.REQUIRED) (
public void insertSecondAuthor() {
Author author = new Author();
author.setName("Alicia Tom");
authorRepository.save(author);
if(new Random().nextBoolean()) {
throw new RuntimeException("DummyException: this should cause rollback of both inserts!");
}
}
Step 1: When the insertFirstAuthor()
method is called, there is no physical transaction. Spring creates one for executing the outer logical transaction—this method’s code.
Step 2: When insertSecondAuthor()
is called from the insertFirstAuthor()
, there is an existing physical transaction. Therefore, Spring invites the inner logical transaction represented by the insertSecondAuthor()
method to participate in this physical transaction.
Step 3: If the RuntimeException
caused randomly at the end of insertSecondAuthor()
method is thrown, then Spring will rollback both logical transactions. Therefore, nothing will be inserted in the database.
The following figure depicts how the Propagation.REQUIRED
flows:
- This is the START point representing the first method call,
insertFirstAuthor()
; - This is the second method call,
insertSecondAuthor()
;
Catching and handling RuntimeException
in insertFirstAuthor()
will still roll back the outer logical transaction. This is happening because the inner logical transaction sets the rollback-only marker, and, since the scopes of both logical transactions are mapped to the same physical transaction, the outer logical transaction is rolled back as well. Spring will silently roll back both logical transactions and then throw the following exception:
xxxxxxxxxx
org.springframework.transaction.UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only.
The outer logical transaction needs to receive an UnexpectedRollbackException
to indicate clearly that a rollback of the inner logical transaction was performed and it should, therefore, be rolled back as well.
Propagation.REQUIRES_NEW
Propagation.REQUIRES_NEW
instructs the Spring container to always create a new physical transaction. Such transactions can also declare their own timeouts, read-only, and isolation level settings and not inherit an outer physical transaction’s characteristics.
The following figure depicts how Propagation.REQUIRES_NEW
flows:
Pay attention to how you handle this aspect since each physical transaction needs its own database connection. So, an outer physical transaction will have its own database connection, while REQUIRES_NEW
will create the inner physical transaction and will bound a new database connection to it. In a synchronous execution, while the inner physical transaction is running, the outer physical transaction is suspended and its database connection remains open. After the inner physical transaction commits, the outer physical transaction is resumed, continuing to run and commit/rollback.
If the inner physical transaction is rolled back, it may or may not affect the outer physical transaction.
xxxxxxxxxx
propagation=Propagation.REQUIRED) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
insertSecondAuthorService.insertSecondAuthor();
}
xxxxxxxxxx
propagation = Propagation.REQUIRES_NEW) (
public void insertSecondAuthor() {
Author author = new Author();
author.setName("Alicia Tom");
authorRepository.save(author);
if(new Random().nextBoolean()) {
throw new RuntimeException ("DummyException: this should cause rollback of second insert only!");
}
}
Step 1: The first physical transaction (outer) is created when you call insertFirstAuthor()
, because there is no existing physical transaction.
Step 2: When the insertSecondAuthor()
is called from insertFirstAuthor()
, Spring will create another physical transaction (inner).
Step 3: If the RuntimeException
is thrown, then both physical transactions (the inner first and outer afterward) are rolled back. This is happening because the exception thrown in insertSecondAuthor()
is propagated to the caller, the insertFirstAuthor()
, therefore causing rollback of the outer physical transaction as well. If this is not the desired behavior, and you want to roll back only the inner physical transaction without affecting the outer physical transaction, you need to catch and handle the RuntimeException
in insertFirstAuthor()
, as shown here:
xxxxxxxxxx
propagation = Propagation.REQUIRED) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
try {
insertSecondAuthorService.insertSecondAuthor();
} catch (RuntimeException e) {
System.err.println("Exception: " + e);
}
}
The outer physical transaction commits even if the inner physical transaction is rolled back.
If the outer physical transaction is rolled back after the inner physical transaction is committed, the inner physical transaction is not affected.
Propagation.NESTED
NESTED
acts like REQUIRED
, only it uses savepoints between nested invocations. In other words, inner logical transactions may roll back independently of outer logical transactions.
Figure following figure depicts how Propagation.NESTED
flows:
Trying to use NESTED
with Hibernate JPA will result in a Spring exception as follows:
xxxxxxxxxx
NestedTransactionNotSupportedException: JpaDialect does not support savepoints
- check your JPA provider capabilities
This is happening because Hibernate JPA doesn’t support nested transactions.
The Spring code that causes the exception is:
xxxxxxxxxx
private SavepointManager getSavepointManager() {
...
SavepointManager savepointManager
= getEntityManagerHolder().getSavepointManager();
if (savepointManager == null) {
throw new NestedTransactionNotSupportedException("JpaDialect does not support ...");
}
return savepointManager;
}
One solution is to use JdbcTemplate
or a JPA provider that supports nested transactions. You may also use jOOQ.
Propagation.MANDATORY
Propagation.MANDATORY
requires an existing physical transaction or will cause an exception, as follows:
xxxxxxxxxx
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'.
The following figure depicts how Propagation.MANDATORY
flows:
Consider the following code:
xxxxxxxxxx
propagation=Propagation.REQUIRED) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
insertSecondAuthorService.insertSecondAuthor();
}
xxxxxxxxxx
propagation = Propagation.MANDATORY) (
public void insertSecondAuthor() {
Author author = new Author();
author.setName("Alicia Tom");
authorRepository.save(author);
if (new Random().nextBoolean()) {
throw new RuntimeException("DummyException: this should cause rollback of both inserts!");
}
}
When insertSecondAuthor()
is called from insertFirstAuthor()
, there is an existing physical transaction (created via Propagation.REQUIRED
). Further, the inner logical transaction represented by the insertSecondAuthor()
code will participate in this physical transaction. If this inner logical transaction is rolled back, then the outer logical transaction is rolled back as well, exactly as with the case of Propagation.REQUIRED
.
Propagation.NEVER
The Propagation.NEVER
states that no physical transaction should exist. If a physical transaction is found, then NEVER
will cause an exception as follows:
xxxxxxxxxx
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
The following figure depicts how Propagation.NEVER
flows:
Check out the following code:
xxxxxxxxxx
propagation = Propagation.NEVER) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
}
Step 1: When insertFirstAuthor()
is called, Spring searches for an existing physical transaction.
Step 2: Since none is available, Spring will not cause an exception and will run this method’s code outside of a physical transaction.
Step 3: When the code reaches the save()
method, Spring will open a physical transaction especially for running this call. This happens because save()
takes advantage of the default Propagation.REQUIRED
.
When you call a method annotated with NEVER
, you must ensure that no physical transaction is open. The code inside this method can open physical transactions with no problem.
Propagation.NOT_SUPPORTED
Propagation.NOT_SUPPORTED
states that if a physical transaction exists, then it will be suspended before continuing. This physical transaction will be automatically resumed at the end. After this transaction is resumed, it can be rolled back (in case of a failure) or committed.
The following figure depicts how Propagation.NOT_SUPPORTED
flows:
Let’s see some code:
xxxxxxxxxx
propagation = Propagation.REQUIRED) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
insertSecondAuthorService.insertSecondAuthor();
}
x
propagation = Propagation.NOT_SUPPORTED) (
public void insertSecondAuthor() {
Author author = new Author();
author.setName("Alicia Tom");
authorRepository.save(author);
if (new Random().nextBoolean()) {
throw new RuntimeException("DummyException: this should cause "
+ "rollback of the insert triggered in insertFirstAuthor() !");
}
}
Step 1: When insertFirstAuthor()
is called, there is no physical transaction available. Therefore, Spring will create a transaction conforming to Propagation.REQUIRED.
Step 2: Further, the code triggers an insert (the author Joana Nimar is persisted in the database).
Step 3: The insertSecondAuthor()
statement is called from insertFirstAuthor()
, and Spring must evaluate the presence of Propagation.NOT_SUPPORTED
. There is an existing physical transaction; therefore, before continuing, Spring will suspend it.
Step 4: The code from insertSecondAuthor()
is executed outside of any physical transaction until the flow hits the save()
call. By default, this method is under the Propagation.REQUIRED
umbrella; therefore, Spring creates a physical transaction, performs the INSERT
(for Alicia Tom), and commits this transaction.
Step 5: The insertSecondAuthor()
remaining code is executed outside of a physical transaction.
Step 6: After the insertSecondAuthor()
code completes, Spring resumes the suspended physical transaction and resumes the execution of the insertFirstAuthor()
logical transaction where it left off. If the RuntimeException
was thrown in insertSecondAuthor()
, then this exception was propagated in insertFirstAuthor()
, and this logical transaction is rolled back.
Even if the transaction is suspended thanks to Propagation.NOT_SUPPORTED
, you should still strive to avoid long-running tasks. Note that while the transaction is suspended, the attached database connection is still active, so the pool connection cannot reuse it. In other words, the database connection is active even when its bounded transaction is suspended:
xxxxxxxxxx
...
Suspending current transaction
HikariPool-1 - Pool stats (total=10, active=1, idle=9, waiting=0)
Resuming suspended transaction after completion of inner transaction
Propagation.SUPPORTS
Propagation.SUPPORTS
states that if a physical transaction exists, then it will execute the demarcated method as a logical transaction in the context of this physical transaction. Otherwise, it will execute this method outside of a physical transaction. Let’s see some code:
xxxxxxxxxx
propagation = Propagation.REQUIRED) (
public void insertFirstAuthor() {
Author author = new Author();
author.setName("Joana Nimar");
authorRepository.save(author);
insertSecondAuthorService.insertSecondAuthor();
}
xxxxxxxxxx
propagation = Propagation.SUPPORTS) (
public void insertSecondAuthor() {
Author author = new Author();
author.setName("Alicia Tom");
authorRepository.save(author);
if (new Random().nextBoolean()) {
throw new RuntimeException("DummyException: this should cause rollback of both inserts!");
}
}
Step 1: When insertFirstAuthor()
is called, there is no physical transaction available. Therefore, Spring will create a transaction conforming to Propagation.REQUIRED
.
Step 2: Further, Spring starts the execution of the outer logical transaction represented by the insertFirstAuthor()
method and triggers an insert via the save()
method (the author Joana Nimar is persisted in the database).
Step 3: The insertSecondAuthor()
is called from insertFirstAuthor()
, and Spring must evaluate the presence of Propagation.SUPPORTS
. There is an existing physical transaction. Therefore, the code from insertSecondAuthor()
is executed as an inner logical transaction in the context of this physical transaction. If a RuntimeException
is thrown, then the inner and the outer logical transactions are rolled back.
Catching and handling the RuntimeException
in insertFirstAuthor()
will still roll back the outer logical transaction. This is happening because the inner logical transaction sets the rollback-only marker and the scopes of both logical transactions are mapped to the same physical transaction.
The following figure depicts how Propagation.SUPPORTS
flows:
Let’s remove @Transactional(propagation = Propagation.REQUIRED)
from insertFirstAuthor()
and evaluate the flow again:
Step 1: When insertFirstAuthor()
is called, there is no physical transaction available, and Spring will not create one because @Transactional
is missing.
Step 2: The code from insertFirstAuthor()
starts to be executed outside of a physical transaction until the flow hits the save()
call. By default, this method is under the Propagation.REQUIRED
umbrella, so Spring creates a physical transaction, performs the insert (for Joana Nimar), and commits this transaction.
Step 3: When insertSecondAuthor()
is called from insertFirstAuthor()
, Spring will need to evaluate the presence of Propagation.SUPPORTS
. There is no physical transaction present and Spring will not create one conforming to the Propagation.SUPPORTS
definition.
Step 4: Until the flow hits the save()
method present in insertSecondAuthor()
, the code is executed outside of a physical transaction. By default, save()
is under the Propagation.REQUIRED
umbrella, so Spring creates a physical transaction, performs the insert (for Alicia Tom), and commits this transaction.
Step 5: When RuntimeException
is thrown, there is no physical transaction, so nothing is rolled back.
The suite of examples used in this appendix is available on GitHub.
If you liked this article, then you'll my book containing 150+ performance items - Spring Boot Persistence Best Practices.
This book helps every Spring Boot developer to squeeze the performances of the persistence layer.
Opinions expressed by DZone contributors are their own.
Comments