Appearance
@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:
- Transaction managers: the classes the advice actually calls to begin, commit and roll back.
- Declarative vs programmatic transactions: the annotation, or code you write yourself, and when the second is the better choice.
- 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:
PlatformTransactionManagerhas the three methods every manager must provide:getTransaction,commit,rollback. The interceptor from part 1 calls exactly these.AbstractPlatformTransactionManagerimplements 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 operationMethod 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:
| Propagation | Parent transaction exists | No parent transaction |
|---|---|---|
REQUIRED (default) | Join it | Create a new one |
REQUIRES_NEW | Suspend it, create a new one; resume the parent afterwards | Create a new one |
SUPPORTS | Join it | Run without a transaction |
NOT_SUPPORTED | Suspend it, run without a transaction, resume the parent | Run without a transaction |
MANDATORY | Join it | Throw an exception |
NEVER | Throw an exception | Run 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=falseSuspending 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
- Which three methods does
PlatformTransactionManagerdefine, and who calls them? - Name four concrete transaction managers and when each is used.
- How do you make
@Transactionaluse a specific transaction manager? - Why can
@Transactionalon a method that calls a slow external API hurt the application? - Show the two programmatic approaches in one line each.
- Method 1 (
@Transactional) calls method 2 (REQUIRED). How many transactions are there? - What does
REQUIRES_NEWdo when a parent transaction exists? Is the parent aborted? - Which propagation throws when there is no parent transaction? Which throws when there is one?
- What is the difference between
SUPPORTSandNOT_SUPPORTEDwhen a parent transaction exists? - How do you set propagation with
TransactionTemplate?
Answers
getTransaction,commit,rollback; theTransactionInterceptor(or your code, programmatically).DataSourceTransactionManager/JdbcTransactionManager(JDBC),JpaTransactionManager(JPA),HibernateTransactionManager(Hibernate API),JtaTransactionManager(distributed transactions).- Define it as a bean and write
@Transactional(transactionManager = "beanName"). - The database connection is held for the whole external call; under load, the connection pool runs out.
status = tm.getTransaction(def); ... tm.commit(status) / tm.rollback(status);andtransactionTemplate.execute(status -> { ...; return null; });.- One: method 2 joins method 1's transaction.
- Suspends the parent, runs in a new transaction, then resumes the parent. No, it is only paused.
MANDATORY.NEVER.SUPPORTSjoins the parent;NOT_SUPPORTEDsuspends it and runs without a transaction.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)(or another value).