Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Spring › Data access and transactions

Transaction propagation

How far does a rollback reach when an exception is thrown?

A method that saves an order calls a method of another bean that saves a log. Both are @Transactional. Which of the two, the order and the log, stays in the database depends on the propagation setting, on where the exception is thrown and what kind it is, and on whether the exception is caught.

Checked against real execution · 72 combinations were run on Spring 7.0.9 and H2 and checked against the results

Concepts in this lab

@Transactional
This annotation is placed on a method or a class so that its scope runs as one transaction. The proxy starts a transaction before the method runs, commits if the method ends normally, and rolls back according to the rules if an exception occurs. You do not have to write the code that starts and ends the transaction yourself, so this is called a declarative transaction.
Transaction propagation
This setting decides what happens when a @Transactional method is called while a transaction is already in progress. The default, REQUIRED, joins the transaction in progress and starts a new one if there is none. REQUIRES_NEW suspends the existing transaction and always starts a separate new transaction. NESTED sets a savepoint inside the same transaction, which makes a partial rollback possible.
Rollback rules and rollbackFor
By default, @Transactional rolls back only when a RuntimeException or an Error is thrown out of the method, and it commits when a checked exception is thrown. To roll back on a checked exception too, specify that exception in the rollbackFor attribute. Also, if you catch and handle an exception inside the method, the proxy cannot know about it, so no rollback happens. However, if that exception was thrown from another @Transactional method that joined the same transaction, the transaction is already marked rollback-only, so even if you catch and handle the exception, an UnexpectedRollbackException occurs at commit time.
Savepoint
This is an intermediate point that you mark while a transaction is in progress. If a problem occurs later, you can roll back only to that point and not the whole transaction. Spring's NESTED propagation uses this feature.
Checked and unchecked exceptions
Unchecked exceptions are RuntimeException and its subclasses (in the specification, the Error classes are also unchecked), and the compiler does not check whether they are handled (examples are NullPointerException and IllegalArgumentException). Checked exceptions are all other Exception classes, and the code compiles only if they are handled with catch or declared with throws (examples are IOException and SQLException). By default, Spring's @Transactional rolls back only on RuntimeException and Error, and commits on checked exceptions.

Try these

Running with the default settings

  1. placeOrder() is called. Before running the method, the proxy starts transaction A (using 1 connection).
  2. The order is inserted with an INSERT. It is not committed yet, so it is visible only inside transaction A.
  3. saveLog() is called. Its propagation setting is REQUIRED and transaction A is already active, so it does not start a new transaction and joins A.
  4. The log is inserted with an INSERT. It belongs to transaction A.
  5. Inside saveLog(), an IllegalStateException is thrown and propagates out of the method.
  6. Proxy of saveLog(): An unchecked exception results in a rollback. But this method only joined A, so it cannot roll back by itself. Instead, it marks A as rollback-only and rethrows the exception.
  7. placeOrder() does not catch the exception, so the IllegalStateException propagates out as it is. Proxy of placeOrder(): An unchecked exception results in a rollback. The proxy rolls back transaction A. All the work done in A is undone.

Result of this run

Code

@Service class OrderService {
    @Transactional
    public void placeOrder() {
        orderDao.insert(order);            // save the order
        logService.saveLog();
    }
}

@Service class LogService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void saveLog() {
        logDao.insert(log);                // save the log
        throw new IllegalStateException();
    }
}

Assumptions of this model

saveLog() is a method of another bean, so it is called through the proxy (when it is called from inside the same class, the inner @Transactional does not apply; the "Proxies and self-invocation" lab covers that case). The rules follow the default behavior of the Spring Framework. A transaction is rolled back only on unchecked exceptions and Error, and when an exception that results in a rollback leaves a participating transaction, the whole transaction is marked rollback-only. NESTED is implemented with JDBC savepoints and works with DataSourceTransactionManager, which JdbcTemplate and MyBatis use. With JPA (JpaTransactionManager + Hibernate), using NESTED throws a NestedTransactionNotSupportedException. This is because a savepoint applies only to the JDBC connection and not to the persistence context. All 72 combinations were run on a real Spring setup (Spring Framework 7.0.9, DataSourceTransactionManager, the H2 database), and it was confirmed that whether the order and the log stay in the database, what the caller gets, and the number of connections in use at once match the screen.

Previous lab: Transaction basics · Next lab: JdbcTemplate and exception translation

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these