Skip to content

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:

  1. The transaction manager hierarchy — what sits under the hood
  2. Declarative vs programmatic transaction management — when to use each
  3. 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 transaction
  • commit(...) — persists changes
  • rollback(...) — 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:

ManagerWhen to Use
DataSourceTransactionManagerPlain JDBC — you write SQL queries manually
JpaTransactionManagerSpring Data JPA — entities, repositories, no manual SQL
HibernateTransactionManagerWhen using Hibernate directly as the ORM
JtaTransactionManagerDistributed 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:

  1. getTransaction(...) — begin
  2. Calls your callback (doInTransaction)
  3. commit(...) on success or rollback(...) 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 methodA started?
  • 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 methodA and methodB run under the same transaction
  • If either throws an exception, the entire transaction rolls back
  • methodB does 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:

  1. methodA's transaction is suspended (paused, not cancelled)
  2. methodB starts a brand new transaction
  3. methodB's transaction commits or rolls back independently
  4. methodA's transaction resumes after methodB finishes

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

PropagationParent ExistsParent Absent
REQUIREDJoin parentCreate new
REQUIRES_NEWSuspend parent, create newCreate new
SUPPORTSJoin parentRun without transaction
NOT_SUPPORTEDSuspend parent, run withoutRun without transaction
MANDATORYJoin parentThrow exception
NEVERThrow exceptionRun 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

ConceptKey Point
PlatformTransactionManagerCore 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
PropagationControls how nested @Transactional calls interact with existing transactions
REQUIREDDefault — join existing or create new
REQUIRES_NEWAlways create new, suspend parent
MANDATORYMust have existing transaction, never creates one
NEVERMust 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.