March 11, 2025

Understanding Propagation and Isolation Levels in Spring Boot Transactions

When working with databases in a Spring Boot application, managing transactions properly is crucial to ensure data consistency, avoid deadlocks, and improve performance. Spring provides two key aspects for controlling transactions: Propagation and Isolation Levels.

This blog post will break down both concepts in a simple way, with real-world and technical examples to help you understand when and why you should use them.


1. Why Do We Need Propagation and Isolation Levels?

Imagine you are withdrawing money from an ATM. You don’t want the system to deduct money from your account unless the cash is dispensed successfully. If the ATM deducts the amount but does not dispense cash due to a technical issue, the system should roll back the transaction.

In software terms, transactions help maintain data consistency by ensuring that either all operations within a transaction are completed successfully or none at all. However, in complex applications, transactions often involve multiple methods and services, requiring fine control over how transactions should behave. This is where Propagation and Isolation Levels come into play.


2. Transaction Propagation in Spring Boot

Transaction propagation defines how a method should run within an existing transaction. Spring provides multiple propagation options, each with a different behavior.

Propagation Type Description When to Use
REQUIRED (Default) Uses an existing transaction or creates a new one if none exists. Most common case where you want a method to participate in a single transaction.
REQUIRES_NEW Always creates a new transaction, suspending any existing transaction. When you need independent transactions (e.g., logging actions separately).
SUPPORTS Uses an existing transaction if available; otherwise, runs non-transactionally. For optional transactions, e.g., read operations where a transaction is not necessary.
NOT_SUPPORTED Runs the method outside of a transaction, suspending any existing one. When a method should not run inside a transaction (e.g., reporting).
MANDATORY Must be executed inside an existing transaction, or else an exception is thrown. When a method should never be executed without an active transaction.
NEVER Must run without a transaction; throws an exception if a transaction exists. Used in cases where transactions must be avoided, like caching operations.
NESTED Runs within a nested transaction that can be rolled back independently. When partial rollbacks are required (e.g., batch processing).

Example of Propagation

Scenario: User Registration with Email Logging

  • When a new user registers, we need to save user details and log the action in a separate table.
  • UserService should complete fully or roll back.
  • LoggingService should always execute, even if the user registration fails.
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private LoggingService loggingService;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void registerUser(User user) {
        userRepository.save(user); // If this fails, rollback
        loggingService.logAction("User registered: " + user.getEmail());
    }
}

@Service
public class LoggingService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAction(String message) {
        // Saves log in a separate transaction
    }
}

If registerUser() fails, the user registration rolls back, but logging will still be recorded due to Propagation.REQUIRES_NEW.


3. Transaction Isolation Levels in Spring Boot

Isolation levels define how transaction operations are isolated from each other to avoid conflicts like dirty reads, non-repeatable reads, and phantom reads.

Isolation Level Description When to Use
DEFAULT Uses the database's default isolation level. General cases where you trust DB settings.
READ_UNCOMMITTED Allows reading uncommitted (dirty) data. Should be avoided unless necessary for performance.
READ_COMMITTED Only committed data can be read. Prevents dirty reads; common choice.
REPEATABLE_READ Prevents dirty and non-repeatable reads but allows phantom reads. Used when multiple consistent reads are required within a transaction.
SERIALIZABLE Fully isolates transactions by locking rows/tables. Highest level of isolation but impacts performance.

Example of Isolation Levels

Scenario: Bank Account Balance Check

  • Suppose two transactions try to update the same bank account balance.
  • If isolation is not managed correctly, a race condition might cause incorrect balance calculations.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(Long fromAccount, Long toAccount, Double amount) {
    Account from = accountRepository.findById(fromAccount).get();
    if (from.getBalance() < amount) {
        throw new InsufficientFundsException();
    }
    from.setBalance(from.getBalance() - amount);
    accountRepository.save(from);
    
    Account to = accountRepository.findById(toAccount).get();
    to.setBalance(to.getBalance() + amount);
    accountRepository.save(to);
}

Using REPEATABLE_READ, we ensure that the balance remains consistent during the transaction.


4. Impact of Using the Wrong Propagation/Isolation Level

Scenario Impact if not handled correctly
Using REQUIRES_NEW unnecessarily Creates unnecessary transactions, reducing performance.
Not using NESTED where needed Causes partial failures instead of isolated rollbacks.
Using READ_UNCOMMITTED in financial transactions Leads to incorrect calculations and security risks.
Not using SERIALIZABLE when required Leads to race conditions and inconsistent data.

5. Real-Life Analogy: Online Shopping Checkout

Consider an e-commerce system:

  • Adding items to the cart (Propagation: REQUIRED) - Should participate in the transaction.
  • Placing an order (Propagation: REQUIRED) - Ensures all order details are saved atomically.
  • Sending an email confirmation (Propagation: REQUIRES_NEW) - Should happen even if the order fails.
  • Updating inventory (Isolation: REPEATABLE_READ) - Ensures stock availability is consistent.

6. Conclusion

Understanding transaction propagation and isolation levels helps you:

  • Avoid data inconsistencies.
  • Improve application performance.
  • Prevent race conditions and deadlocks.

Choosing the right settings depends on the business scenario. A well-configured transaction management strategy ensures reliable and efficient operations in a Spring Boot application.


Got questions? Comment below! 🚀