Appearance
Spring Boot @Transactional Annotation Part 2 | Declarative and Programmatic Approach, Propagation
Recap and What Is Ahead
In the previous chapter, you learned what transactions are, why ACID properties matter, and how Spring Boot uses AOP internally to wrap your methods with begin, commit, and rollback logic via TransactionInterceptor. You also saw that simply putting @Transactional on a method is enough to get all of that for free.
Now let us go deeper. This chapter covers:
- The transaction manager hierarchy — what sits under the hood
- Declarative vs programmatic transaction management — when to use each
- Transaction propagation — the most interview heavy concept in this entire topic
Real World Analogy: The Restaurant Kitchen
Think of a large restaurant chain. The head chef (the PlatformTransactionManager interface) defines rules: every cook must know how to start a dish, complete it, or throw it away. Individual stations — grill station, pasta station, pastry station — each have their own way of cooking, but they all follow the head chef's rules. Spring's transaction manager hierarchy works the same way.
The Transaction Manager Hierarchy
Understanding this hierarchy helps you know which class to configure when you need to go beyond defaults.
Layer 1: TransactionManager
java
public interface TransactionManager {
// This is intentionally empty — it is a marker interface
}It is the root of the hierarchy. Nothing useful here directly.
Layer 2: PlatformTransactionManager
java
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}This is the critical interface. The three methods here are exactly what TransactionInterceptor calls:
getTransaction(...)— begins a transactioncommit(...)— persists changesrollback(...)— reverses changes
Layer 3: AbstractPlatformTransactionManager
This is an abstract class that provides a default implementation of the three methods above. Most concrete transaction managers share the same logic for get, commit, and rollback, so this parent class implements the common parts. Subclasses override only what is specific to their database technology.
Layer 4: Concrete Transaction Managers
These are the actual implementations you use:
| Manager | When to Use |
|---|---|
DataSourceTransactionManager | Plain JDBC — you write SQL queries manually |
JpaTransactionManager | Spring Data JPA — entities, repositories, no manual SQL |
HibernateTransactionManager | When using Hibernate directly as the ORM |
JtaTransactionManager | Distributed transactions across multiple databases (two phase commit) |
When you use Spring Boot with JPA, Spring Boot automatically selects JpaTransactionManager unless you tell it otherwise.
Two Types of Transaction Management
Type 1: Declarative Transaction Management
This is the @Transactional annotation you have already seen. You declare intent through the annotation, and Spring handles all the mechanics.
java
@Service
public class UserService {
@Transactional // declarative — you declare, Spring manages
public void updateUser(Long id, String name) {
userRepository.updateName(id, name);
userRepository.updateStatus(id, "ACTIVE");
}
}Spring Boot automatically selects the right transaction manager based on what is on the classpath. In most JPA applications it will be JpaTransactionManager.
Specifying Which Transaction Manager to Use
If you need to override the default and use a specific transaction manager, create a bean and reference it by name:
java
// AppConfig.java
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public PlatformTransactionManager userTransactionManager() {
// explicitly using JDBC DataSource manager, not JPA
return new DataSourceTransactionManager(dataSource);
}
}Then reference it by the bean name (the method name becomes the bean name):
java
@Service
public class UserService {
// tells Spring: use the bean named "userTransactionManager"
@Transactional(transactionManager = "userTransactionManager")
public void updateUser(Long id, String name) {
// runs under DataSourceTransactionManager, not JpaTransactionManager
}
}Type 2: Programmatic Transaction Management
Programmatic transaction management means you write the transaction lifecycle in your own code. It is more flexible but harder to maintain at scale.
When does programmatic make sense?
Consider a service method that does this:
1. Update database records (initial DB operations)
2. Call an external third party API (takes 3 to 4 seconds)
3. Update database records again (final DB operations)If you put @Transactional on the whole method, the database connection is held open during the external API call. Under peak traffic, this means hundreds of open connections waiting on a slow network call — your database connection pool gets exhausted.
With programmatic transaction management, you can create a transaction for step 1, close it, make the API call outside any transaction, and then open a new transaction for step 3. This dramatically reduces how long you hold database connections.
Approach 1: Manual PlatformTransactionManager
java
@Service
public class UserService {
private final PlatformTransactionManager txManager;
// Constructor injection — txManager is wired from the bean in AppConfig
public UserService(PlatformTransactionManager userTransactionManager) {
this.txManager = userTransactionManager;
}
public void updateUserProgrammatic(Long userId, String name) {
// Step 1: Define transaction properties
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("updateUserTransaction");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// Step 2: Begin transaction
TransactionStatus status = txManager.getTransaction(def);
try {
// Step 3: Your actual business logic
userRepository.updateName(userId, name);
userRepository.updateStatus(userId, "ACTIVE");
// Step 4: Commit on success
txManager.commit(status);
} catch (Exception e) {
// Step 5: Roll back on failure
txManager.rollback(status);
throw e;
}
}
}This works but is verbose. Every method that needs transactions must duplicate this try/catch/commit/rollback pattern.
Approach 2: TransactionTemplate (Cleaner Programmatic Approach)
Spring provides TransactionTemplate as a wrapper that hides the get, commit, and rollback mechanics. You only supply the business logic as a callback.
First, declare the TransactionTemplate bean:
java
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public PlatformTransactionManager userTransactionManager() {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate() {
TransactionTemplate template = new TransactionTemplate(userTransactionManager());
// optionally configure propagation and name
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
template.setName("myTemplate");
return template;
}
}Now use it in your service:
java
@Service
public class UserService {
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserRepository userRepository;
public void updateUserWithExternalCall(Long userId) {
// Transaction 1: initial DB operations only
transactionTemplate.execute(status -> {
// This is a TransactionCallback — your business logic goes here
userRepository.markProcessing(userId);
userRepository.logStartTime(userId);
return null; // return a value or null
});
// External API call — NO active transaction here
// The database connection is NOT held during this slow call
String result = externalApiService.fetchData(userId); // takes 3-4 seconds
// Transaction 2: final DB operations only
transactionTemplate.execute(status -> {
userRepository.updateResult(userId, result);
userRepository.markCompleted(userId);
return null;
});
}
}TransactionTemplate.execute(...) internally does:
getTransaction(...)— begin- Calls your callback (
doInTransaction) commit(...)on success orrollback(...)on exception
You get the cleanliness of not writing rollback/commit, with the flexibility of controlling exactly which code runs inside a transaction.
Transaction Propagation
Propagation is arguably the most important and most tested concept in Spring transaction management.
The Core Question
Suppose methodA() is annotated with @Transactional and internally it calls methodB(), which is also annotated with @Transactional. When methodB tries to begin a transaction, what should happen?
- Should it join the existing transaction that
methodAstarted? - Should it create a new transaction of its own?
- Should it refuse to run if no transaction exists?
That is exactly what propagation controls.
How It Works Internally
When TransactionInterceptor intercepts a method annotated with @Transactional, it calls getTransaction(definition). Inside this method, Spring checks the propagation value and applies the appropriate behavior. The method createTransactionIfNecessary inside TransactionAspectSupport (the parent of TransactionInterceptor) handles all the propagation logic.
Setting Propagation
java
@Transactional(propagation = Propagation.REQUIRED)
public void someMethod() { ... }Propagation Types Explained
REQUIRED (default)
java
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() { ... }Rule: If a parent transaction exists, join it. If none exists, create a new one.
This is the default. When methodA calls methodB:
- Both
methodAandmethodBrun under the same transaction - If either throws an exception, the entire transaction rolls back
methodBdoes not start its own transaction if one already exists
Proof from logs:
methodA transaction name: com.example.UserService.methodA
methodB transaction name: com.example.UserService.methodA ← same transaction!REQUIRES_NEW
java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() { ... }Rule: Always create a new transaction. If a parent transaction exists, suspend it temporarily.
When methodA calls methodB:
methodA's transaction is suspended (paused, not cancelled)methodBstarts a brand new transactionmethodB's transaction commits or rolls back independentlymethodA's transaction resumes aftermethodBfinishes
Use case: Audit logging. You want to save an audit record even if the main operation rolls back. Put the audit save in REQUIRES_NEW — it commits independently.
Proof from logs:
methodA transaction name: com.example.UserService.methodA
methodB transaction name: com.example.UserService.methodB ← different transaction!SUPPORTS
java
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() { ... }Rule: If a parent transaction exists, join it. If none exists, run without any transaction.
Use this for read operations that can work fine with or without a transaction context.
NOT_SUPPORTED
java
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() { ... }Rule: Always run without a transaction. If a parent transaction exists, suspend it while methodB runs, then resume it after.
Use this when a method must not run inside a transaction (e.g., it calls stored procedures that cannot run inside an active transaction).
MANDATORY
java
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() { ... }Rule: A parent transaction must exist. If none exists, throw IllegalTransactionStateException.
This propagation never creates a transaction on its own. It demands that the caller already has one. Use it when a method should only ever be called within a transactional context and you want to make that contract explicit.
NEVER
java
@Transactional(propagation = Propagation.NEVER)
public void methodB() { ... }Rule: Must run without any transaction. If a parent transaction exists, throw an exception.
The opposite of MANDATORY. This method enforces that it is never called from a transactional context.
Propagation in Programmatic Approach
If you use the manual approach (PlatformTransactionManager), set propagation via TransactionDefinition:
java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("auditTransaction");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = txManager.getTransaction(def);If you use TransactionTemplate, set it on the template bean:
java
@Bean
public TransactionTemplate auditTransactionTemplate() {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
template.setName("auditTemplate");
return template;
}Propagation Quick Reference
| Propagation | Parent Exists | Parent Absent |
|---|---|---|
REQUIRED | Join parent | Create new |
REQUIRES_NEW | Suspend parent, create new | Create new |
SUPPORTS | Join parent | Run without transaction |
NOT_SUPPORTED | Suspend parent, run without | Run without transaction |
MANDATORY | Join parent | Throw exception |
NEVER | Throw exception | Run without transaction |
Complete Example: Propagation in Action
java
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Autowired
private AuditService auditService;
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
// runs inside transaction T1
orderRepository.save(order);
// paymentService runs inside T1 (REQUIRED joins existing)
paymentService.processPayment(order.getPayment());
// auditService runs in a NEW transaction T2 (REQUIRES_NEW)
// even if placeOrder() rolls back, the audit record is saved
auditService.logOrderPlaced(order.getId());
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRED)
public void processPayment(Payment payment) {
// joins the caller's transaction T1
paymentRepository.save(payment);
chargeGateway(payment); // if this throws, T1 rolls back
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderPlaced(Long orderId) {
// runs in its own transaction T2 — independent of T1
auditRepository.save(new AuditLog("ORDER_PLACED", orderId));
// commits T2 immediately, regardless of what T1 does
}
}Summary
| Concept | Key Point |
|---|---|
PlatformTransactionManager | Core interface with getTransaction, commit, rollback |
| Declarative | @Transactional annotation — Spring manages everything |
| Programmatic (manual) | Use PlatformTransactionManager directly — full control |
| Programmatic (template) | Use TransactionTemplate — cleaner, wraps the boilerplate |
| Propagation | Controls how nested @Transactional calls interact with existing transactions |
REQUIRED | Default — join existing or create new |
REQUIRES_NEW | Always create new, suspend parent |
MANDATORY | Must have existing transaction, never creates one |
NEVER | Must NOT have existing transaction |
Interview Questions
Q1: What is the difference between declarative and programmatic transaction management?
Declarative uses @Transactional — Spring handles begin, commit, and rollback via AOP transparently. Programmatic uses PlatformTransactionManager or TransactionTemplate directly in code. Declarative is simpler and preferred for most cases. Programmatic is useful when you need fine grained control, such as managing transaction boundaries around external API calls to avoid holding database connections too long.
Q2: What is propagation in Spring transactions?
Propagation defines the behavior of a transactional method when called from another transactional method. It controls whether the called method joins the existing transaction, creates a new independent transaction, or runs without any transaction at all.
Q3: What is the default propagation and what does it do?
The default is PROPAGATION_REQUIRED. If a transaction already exists, the method joins it. If no transaction exists, a new one is created.
Q4: What is the difference between REQUIRED and REQUIRES_NEW?
REQUIRED joins the caller's transaction if one exists — both caller and callee share the same transaction, and any exception in either causes the whole thing to roll back. REQUIRES_NEW always creates a new transaction and suspends the caller's transaction. The two transactions are completely independent — REQUIRES_NEW can commit even if the caller rolls back, which makes it ideal for audit logs.
Q5: When would you use TransactionTemplate over PlatformTransactionManager directly?
TransactionTemplate is a cleaner programmatic approach. It wraps the get/commit/rollback boilerplate so you only provide business logic as a callback. Use it when you need programmatic transactions but want to avoid repeating the try/catch/rollback pattern in every method.
Q6: Why would you use programmatic transactions at all if declarative is simpler?
When a service method mixes DB operations with slow external API calls, using @Transactional on the whole method holds the database connection open during the API call. Under load this exhausts the connection pool. With programmatic transactions you can open a transaction for the first DB block, close it, make the API call, and then open a new transaction for the final DB block — minimizing connection hold time.
Q7: What is MANDATORY propagation used for?
MANDATORY ensures a method is only called from within an existing transaction. If no transaction exists when the method is called, it throws IllegalTransactionStateException. This is useful for enforcing transactional contracts — marking internal helper methods that must never be called outside a transaction context.
Q8: How do you specify which transaction manager to use with @Transactional?
Use @Transactional(transactionManager = "beanName") where beanName is the name of the PlatformTransactionManager bean you want to use. If you do not specify, Spring Boot auto selects based on the classpath — typically JpaTransactionManager for JPA applications.