Skip to content
Chapter 14 · Spring Boot

@Transactional Part 2: Transaction Managers and Propagation ​

Part 1 showed what @Transactional does: an around advice that begins a transaction, runs your method, and commits or rolls back. This part goes one level deeper, in three steps:

  1. Transaction managers: the classes the advice actually calls to begin, commit and roll back.
  2. Declarative vs programmatic transactions: the annotation, or code you write yourself, and when the second is the better choice.
  3. Propagation: what happens when one transactional method calls another.

Transaction managers ​

The advice from part 1 creates, commits and rolls back transactions through a transaction manager:

  • PlatformTransactionManager has the three methods every manager must provide: getTransaction, commit, rollback. The interceptor from part 1 calls exactly these.
  • AbstractPlatformTransactionManager implements the common logic (for example, propagation, below); each concrete manager overrides only what is specific to it.
  • Concrete managers for each technology:
    • DataSourceTransactionManager / JdbcTransactionManager: plain JDBC, where you write the SQL yourself.
    • JpaTransactionManager: JPA, where entities map to tables and most CRUD needs no SQL.
    • HibernateTransactionManager: Hibernate's own API.
    • JtaTransactionManager: JTA (Java Transaction API), for distributed transactions across several resources (two-phase commit).

For local transactions on a relational database, any of the first three works; NoSQL stores bring their own managers through their starters.

Declarative transactions ​

Declarative means with the annotation: @Transactional. Spring Boot hides the details and picks a transaction manager for you based on what is on the classpath; with Spring Data JPA, it is a JpaTransactionManager.

To choose the manager yourself, define it as a bean and name it in the annotation:

java
@Configuration
public class AppConfig {

    @Bean
    public PlatformTransactionManager userTransactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
java
@Component
public class UserDeclarative {

    @Transactional(transactionManager = "userTransactionManager")
    public void updateUserDeclarative() {
        System.out.println("Insert Query ran");
        System.out.println("Update Query ran");
    }
}

transactionManager is the bean name, which by default is the @Bean method's name. The DataSource (the connection to the database, configured by Spring Boot from application.properties; chapter 23) is injected as a method parameter.

One side effect to know. Spring Boot creates its own manager, a bean named transactionManager (a JpaTransactionManager when JPA is present), only if you define none. Listing the PlatformTransactionManager beans shows it:

text
without AppConfig's manager:   [transactionManager]
with AppConfig's manager:      [userTransactionManager]

Once you declare userTransactionManager, Boot's JPA manager is gone, and every @Transactional without a transactionManager attribute now uses yours. In an application that also uses JPA, keep a JPA manager too (or don't define your own).

Programmatic transactions ​

Programmatic means managing the transaction in your own code. It is more flexible, but harder to maintain when used in many places.

Why flexible? Consider:

java
@Transactional
public void updateUser() {
    // 1. initial DB operations
    // 2. call an external API (may take 3-4 seconds)
    // 3. final DB operations
}

With @Transactional on the whole method, the transaction, and the database connection it holds, stay open for the whole external call. Under peak traffic many connections sit idle waiting on the external service, the connection pool runs dry, and the whole application slows down. The best fix is to move the external call out of the method. If the three steps must stay in one method, programmatic transactions let you put a transaction around only the database parts:

java
public void updateUser() {
    transactionTemplate.execute(status -> { /* 1. initial DB operations */ return null; });
    externalApi.call();                                  // no transaction, no connection held
    transactionTemplate.execute(status -> { /* 3. final DB operations */ return null; });
}

Printing the transaction state in each step (with TransactionTemplate, approach 2 below):

text
template part 1 (DB): is transaction active: true, current transaction name: TRANSACTION TEMPLATE REQUIRED PROPAGATION
external API call, outside any transaction: false
template part 2 (DB): is transaction active: true, current transaction name: TRANSACTION TEMPLATE REQUIRED PROPAGATION

(Each part is its own transaction: if part 3 fails, part 1 is not rolled back. Decide whether that is acceptable for your use case.)

Approach 1: with the transaction manager ​

java
@Component
public class UserDAO {

    PlatformTransactionManager userTransactionManager;

    UserDAO(PlatformTransactionManager userTransactionManager) {
        this.userTransactionManager = userTransactionManager;
    }

    public void updateUserProgrammatic() {
        TransactionStatus status = userTransactionManager.getTransaction(null);   // BEGIN
        try {
            System.out.println("Insert Query ran");
            System.out.println("Update Query ran");
            userTransactionManager.commit(status);                               // COMMIT
        } catch (Exception e) {
            userTransactionManager.rollback(status);                             // ROLLBACK
        }
    }
}

getTransaction(null) uses default settings. To name the transaction or set its propagation, pass a definition:

java
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setName("Testing REQUIRED propagation");
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = userTransactionManager.getTransaction(definition);

Approach 2: TransactionTemplate ​

The begin/commit/rollback code above is still yours. TransactionTemplate wraps it: it gets a transaction, runs your code, and commits or rolls back.

java
@Configuration
public class AppConfig {

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager userTransactionManager) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(userTransactionManager);
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        transactionTemplate.setName("TRANSACTION TEMPLATE REQUIRED PROPAGATION");
        return transactionTemplate;
    }
}
java
@Component
public class UserDAO {

    @Autowired
    TransactionTemplate transactionTemplate;

    public void updateUserProgrammatic() {
        TransactionCallback<Void> dbOperationTask = status -> {
            System.out.println("Insert Query ran");
            System.out.println("Update Query ran");
            return null;
        };
        transactionTemplate.execute(dbOperationTask);
    }
}

TransactionCallback is a functional interface (its method is doInTransaction), so a lambda carries your business logic in.

Propagation ​

A @Transactional method (method 1) calls another @Transactional method (method 2). Does method 2 join method 1's transaction or start its own? Each time a transaction is requested, the manager checks the propagation setting to decide.

java
@Transactional(propagation = Propagation.REQUIRED)

To see it, print the transaction state inside each method:

java
System.out.println("Is transaction active: " + TransactionSynchronizationManager.isActualTransactionActive());
System.out.println("Current transaction name: " + TransactionSynchronizationManager.getCurrentTransactionName());

The name of an annotation-driven transaction is the method that started it.

A demo. Method 1 starts a transaction, does some work, and calls method 2 in another bean:

java
@Component
public class UserDeclarative {

    @Autowired
    UserDeclarative2 userDeclarative2;

    @Transactional
    public void updateUser() {
        // print transaction state
        System.out.println("some initial DB operation");
        userDeclarative2.updateUserFromMethod2();
        System.out.println("some final DB operation");
    }
}

@Component
public class UserDeclarative2 {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUserFromMethod2() {
        // print transaction state
    }
}
text
method1 (updateUser): is transaction active: true, current transaction name: com.example.demo.UserDeclarative.updateUser
some initial DB operation
method2 (REQUIRES_NEW): is transaction active: true, current transaction name: com.example.demo.UserDeclarative2.updateUserFromMethod2
some final DB operation

Method 2 ran in a different transaction (its own name). With REQUIRED instead, method 2 would print method 1's name: it joins the existing transaction. (Method 2 must be in another bean: a call within the same class bypasses the proxy, part 1.)

The full rule set:

PropagationParent transaction existsNo parent transaction
REQUIRED (default)Join itCreate a new one
REQUIRES_NEWSuspend it, create a new one; resume the parent afterwardsCreate a new one
SUPPORTSJoin itRun without a transaction
NOT_SUPPORTEDSuspend it, run without a transaction, resume the parentRun without a transaction
MANDATORYJoin itThrow an exception
NEVERThrow an exceptionRun without a transaction

Tested, with method 1 @Transactional calling method 2 with each setting:

REQUIRED       active=true  name=...Outer.withParent     (the parent's transaction)
REQUIRES_NEW   active=true  name=...Inner.requiresNew    (a new one; the parent resumes afterwards)
SUPPORTS       active=true  name=...Outer.withParent
NOT_SUPPORTED  active=false
MANDATORY      active=true  name=...Outer.withParent
NEVER          IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

and called without a parent transaction:

REQUIRED       active=true  name=...Inner.required
REQUIRES_NEW   active=true  name=...Inner.requiresNew
SUPPORTS       active=false
NOT_SUPPORTED  active=false
MANDATORY      IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
NEVER          active=false

Suspending is not aborting. With REQUIRES_NEW, the parent transaction waits; the new one commits or rolls back on its own; then the parent continues. So a REQUIRES_NEW method's work is kept even if the parent later rolls back, which is useful for audit logs.

This decision logic lives in AbstractPlatformTransactionManager.getTransaction, in the branch for an existing transaction (handleExistingTransaction): NEVER throws, NOT_SUPPORTED suspends, REQUIRES_NEW suspends and starts a new one, and so on.

(A seventh value, NESTED, runs inside the parent using a savepoint, so it can roll back on its own without undoing the parent. It depends on the transaction manager and database supporting savepoints.)

With the programmatic approaches, set propagation through DefaultTransactionDefinition.setPropagationBehavior(...) or TransactionTemplate.setPropagationBehavior(...), as shown above.

Next: Propagation decides whether methods share a transaction. Part 3 covers the other main attribute, isolation: what one transaction sees of another running at the same time.

Check yourself ​

  1. Which three methods does PlatformTransactionManager define, and who calls them?
  2. Name four concrete transaction managers and when each is used.
  3. How do you make @Transactional use a specific transaction manager?
  4. Why can @Transactional on a method that calls a slow external API hurt the application?
  5. Show the two programmatic approaches in one line each.
  6. Method 1 (@Transactional) calls method 2 (REQUIRED). How many transactions are there?
  7. What does REQUIRES_NEW do when a parent transaction exists? Is the parent aborted?
  8. Which propagation throws when there is no parent transaction? Which throws when there is one?
  9. What is the difference between SUPPORTS and NOT_SUPPORTED when a parent transaction exists?
  10. How do you set propagation with TransactionTemplate?
Answers
  1. getTransaction, commit, rollback; the TransactionInterceptor (or your code, programmatically).
  2. DataSourceTransactionManager/JdbcTransactionManager (JDBC), JpaTransactionManager (JPA), HibernateTransactionManager (Hibernate API), JtaTransactionManager (distributed transactions).
  3. Define it as a bean and write @Transactional(transactionManager = "beanName").
  4. The database connection is held for the whole external call; under load, the connection pool runs out.
  5. status = tm.getTransaction(def); ... tm.commit(status) / tm.rollback(status); and transactionTemplate.execute(status -> { ...; return null; });.
  6. One: method 2 joins method 1's transaction.
  7. Suspends the parent, runs in a new transaction, then resumes the parent. No, it is only paused.
  8. MANDATORY. NEVER.
  9. SUPPORTS joins the parent; NOT_SUPPORTED suspends it and runs without a transaction.
  10. transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW) (or another value).