Spring Transaction Propagation Behavior Types and Usage

In the Spring Framework, transaction propagation defines how a transaction boundary behaves when a transactional method is invoked by another transactional method. It is the core configuration of the propagation attribute within the @Transactional annotation, and it directly dictates how data consistency is maintained when multiple business services interact.

Because Spring's transaction management is implemented via AOP (Aspect-Oriented Programming) proxies, the framework must decide whether a new method should join an existing transaction, suspend the current one to start a fresh one, or run without any transaction at all.
Spring provides seven distinct propagation behaviors via the Propagation enum. Understanding these is essential for designing robust, atomic business processes.

  • REQUIRED (Default): This is the most common setting. If a transaction already exists, the current method joins it. If no transaction exists, a new one is created.
  • REQUIRES_NEW: This always starts a new, independent transaction. If a transaction is already running, it is suspended until the new transaction completes (either commits or rolls back). The inner transaction's outcome does not affect the outer one, and vice versa.
  • NESTED: This creates a "nested" transaction using JDBC Savepoints. If a transaction exists, the method runs within a sub-transaction. If the nested transaction fails, it rolls back to the savepoint, allowing the outer transaction to continue. If no transaction exists, it behaves like REQUIRED.
  • SUPPORTS: The method will run within a transaction if one is present. If no transaction exists, it executes non-transactionally.
  • NOT_SUPPORTED: The method always runs non-transactionally. If a transaction is currently active, it is suspended for the duration of the method execution.
  • MANDATORY: The method requires an existing transaction. If no transaction is active, Spring throws an IllegalTransactionStateException.
  • NEVER: The method must run without a transaction. If a transaction is active, Spring throws an IllegalTransactionStateException.

Among these, REQUIRED, REQUIRES_NEW, and NESTED are the most frequently utilized in production environments.

Practical Implementation: An Order Processing Scenario

To understand how these behave in a real-world application, consider an e-commerce scenario where we need to create an order, deduct stock, and log the outcome.

In this example, the stock deduction must be part of the main order transaction (REQUIRED). However, if the order fails, we still want to record the failure in a log table. To ensure the log is saved even if the main transaction rolls back, we use REQUIRES_NEW for the logging service.

@Service
public class OrderService {

    @Autowired
    private StockService stockService;
    @Autowired
    private LogService logService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder(Order order) {
        try {
            // This must succeed for the order to be valid
            stockService.deductStock(order);
        } catch (Exception e) {
            // We use REQUIRES_NEW here so the log is persisted 
            // even if the createOrder transaction rolls back.
            logService.recordFailure(order, e);
            throw e; // Re-throw to trigger rollback of the main transaction
        }
    }
}

@Service
public class StockService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void deductStock(Order order) {
        stockMapper.updateStock(order.getProductId(), -order.getCount());
    }
}

@Service
public class LogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordFailure(Order order, Exception e) {
        logMapper.insert(new OpLog(order.getId(), e.getMessage()));
    }
}

Key Distinction: REQUIRES_NEW vs. NESTED

A common point of confusion for developers is the difference between REQUIRES_NEW and NESTED. While both allow for partial rollbacks, their underlying mechanics are fundamentally different:

  1. Transaction Independence:

    • REQUIRES_NEW creates two entirely separate physical transactions. They use different database connections. If the inner transaction commits, it is permanent, even if the outer transaction later rolls back.
    • NESTED uses a single physical transaction but utilizes Savepoints. The inner transaction is a logical sub-unit. If the inner transaction rolls back, it only rolls back to the savepoint. However, if the outer transaction rolls back, the inner transaction is also rolled back.
  2. Resource Usage:

    • REQUIRES_NEW is more "expensive" because it requires an additional database connection from the pool while the first one is suspended.
    • NESTED is more resource-efficient as it operates within the same connection.

Critical Implementation Pitfalls

Even with a clear understanding of propagation, several common mistakes can lead to unexpected behavior:

  • The Self-Invocation Problem: This is the most frequent error. Because Spring uses AOP proxies, calling a @Transactional method from another method within the same class bypasses the proxy. Consequently, the propagation settings will be ignored. To fix this, you should move the transactional method to a separate Bean or inject the bean into itself (self-autowiring).
  • Rollback Rules and Exceptions: By default, Spring only triggers a rollback for unchecked exceptions (RuntimeException and Error). If your code throws a checked exception (e.g., IOException), the transaction will commit unless you explicitly configure it using @Transactional(rollbackFor = Exception.class).
  • Connection Pool Exhaustion: When using REQUIRES_NEW, the outer transaction holds its connection while waiting for the inner transaction to finish. In high-concurrency environments, this can lead to deadlocks or quickly exhaust the database connection pool.
  • NESTED Compatibility: Since NESTED relies on JDBC Savepoints, you must ensure that your underlying data source and persistence framework (like certain JPA configurations) fully support this mechanism.

Summary Decision Guide

When designing your service layer, you can follow these general principles:

  • Use REQUIRED for standard business logic where all steps must succeed or fail together.
  • Use REQUIRES_NEW for independent tasks like auditing, logging, or sending notifications that must persist regardless of the main business outcome.
  • Use NESTED for batch processing where you want to attempt several operations and, if one fails, roll back only that specific attempt and continue with the rest.
  • Use MANDATORY when a method is a helper that should never be called outside of a controlled transactional context.
  • Use SUPPORTS or NOT_SUPPORTED for read-only operations or performance-sensitive tasks where transaction overhead is unnecessary.