{{announcement.body}}
{{announcement.title}}

Spring Transaction Propagation in a Nutshell

DZone 's Guide to

Spring Transaction Propagation in a Nutshell

Diving into Spring Transaction Propagation mechanism

· Java Zone ·
Free Resource

Spring 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):

Java
 




x


1
@Transactional(propagation=Propagation.REQUIRED)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
  authorRepository.save(author);
7
     
8
  insertSecondAuthorService.insertSecondAuthor();
9
}


Java
 




x


 
1
@Transactional(propagation = Propagation.REQUIRED)
2
public void insertSecondAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Alicia Tom");
6
    
7
  authorRepository.save(author);
8
     
9
  if(new Random().nextBoolean()) {
10
    throw new RuntimeException("DummyException: this should cause rollback of both inserts!");
11
  }
12
}



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:

  1. This is the START point representing the first method call, insertFirstAuthor()
  2. This is the second method call, insertSecondAuthor();

Propogation.REQUIRED workflow

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: 

Plain Text
 




xxxxxxxxxx
1


 
1
org.springframework.transaction.UnexpectedRollbackException: 
2
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:

Propogation.REQUIRES_NEW workflow

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.

Java
 




xxxxxxxxxx
1
10
9
10


1
@Transactional(propagation=Propagation.REQUIRED)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
    
7
  authorRepository.save(author);
8
     
9
  insertSecondAuthorService.insertSecondAuthor();
10
}


Java
 




xxxxxxxxxx
1
10
9
10


1
@Transactional(propagation = Propagation.REQUIRES_NEW)
2
public void insertSecondAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Alicia Tom");
6
    
7
  authorRepository.save(author);
8
    
9
  if(new Random().nextBoolean()) {    
10
    throw new RuntimeException ("DummyException: this should cause rollback of second insert only!");
11
  }
12
}



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:

Java
 




xxxxxxxxxx
1
11


1
@Transactional(propagation = Propagation.REQUIRED)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
    
7
  authorRepository.save(author);
8
    
9
  try {
10
    insertSecondAuthorService.insertSecondAuthor();
11
  } catch (RuntimeException e) {
12
    System.err.println("Exception: " + e);
13
  }
14
}



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:

Propogation.NESTED workflow

Trying to use NESTED with Hibernate JPA will result in a Spring exception as follows:

Plain Text
 




xxxxxxxxxx
1


 
1
NestedTransactionNotSupportedException: JpaDialect does not support savepoints 
2
- check your JPA provider capabilities



This is happening because Hibernate JPA doesn’t support nested transactions.

The Spring code that causes the exception is:

Java
 




xxxxxxxxxx
1
10
9
10
9
10
9
10
9
10
9
10
9
10


1
private SavepointManager getSavepointManager() {
2
  ...
3
  SavepointManager savepointManager
4
    = getEntityManagerHolder().getSavepointManager();
5
    
6
  if (savepointManager == null) {
7
    throw new NestedTransactionNotSupportedException("JpaDialect does not support ...");
8
  }
9
    
10
  return savepointManager;
11
}



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:

Plain Text
 




xxxxxxxxxx
1


1
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'.



The following figure depicts how Propagation.MANDATORY flows:

Propogation.MANDATORY workflow

Consider the following code:

Java
 




xxxxxxxxxx
1
10


 
1
@Transactional(propagation=Propagation.REQUIRED)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
    
7
  authorRepository.save(author);
8
    
9
  insertSecondAuthorService.insertSecondAuthor();
10
}


Java
 




xxxxxxxxxx
1
10
9
10


1
@Transactional(propagation = Propagation.MANDATORY)
2
public void insertSecondAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Alicia Tom");
6
   
7
  authorRepository.save(author);
8
    
9
  if (new Random().nextBoolean()) {
10
    throw new RuntimeException("DummyException: this should cause rollback of both inserts!");
11
  }
12
}



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:

Plain Text
 




xxxxxxxxxx
1


1
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'



The following figure depicts how Propagation.NEVER flows:

Propogation.NEVER workflow

Check out the following code:

Java
 




xxxxxxxxxx
1


1
@Transactional(propagation = Propagation.NEVER)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
    
7
  authorRepository.save(author);
8
}



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:

Propogation.NOT_SUPPORTED

Let’s see some code:

Java
 




xxxxxxxxxx
1
10
9
10


 
1
@Transactional(propagation = Propagation.REQUIRED)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
      
7
  authorRepository.save(author);
8
    
9
  insertSecondAuthorService.insertSecondAuthor();
10
}


Java
 




x
9
10


1
@Transactional(propagation = Propagation.NOT_SUPPORTED)
2
public void insertSecondAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Alicia Tom");  
6
    
7
  authorRepository.save(author);
8
    
9
  if (new Random().nextBoolean()) {
10
    throw new RuntimeException("DummyException: this should cause "
11
                               + "rollback of the insert triggered in insertFirstAuthor() !");
12
  }
13
}



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:

Plain Text
 




xxxxxxxxxx
1


 
1
...
2
Suspending current transaction
3
HikariPool-1 - Pool stats (total=10, active=1, idle=9, waiting=0)
4
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:

Java
 




xxxxxxxxxx
1
10


 
1
@Transactional(propagation = Propagation.REQUIRED)
2
public void insertFirstAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Joana Nimar");
6
    
7
  authorRepository.save(author);
8
    
9
  insertSecondAuthorService.insertSecondAuthor();
10
}


Java
 




xxxxxxxxxx
1
10
9
10


1
@Transactional(propagation = Propagation.SUPPORTS)
2
public void insertSecondAuthor() {
3
    
4
  Author author = new Author();
5
  author.setName("Alicia Tom");
6
    
7
  authorRepository.save(author);
8
    
9
  if (new Random().nextBoolean()) {
10
    throw new RuntimeException("DummyException: this should cause rollback of both inserts!");
11
  }
12
}



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:

Propogation.SUPPORTS workflow

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.  

Topics:
hibernate, java, jpa, persistence, spring, springboot, springdata

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}