Mastering Spring Boot Transaction Rollback: A Complete Guide

Spring Boot simplifies transaction management via PlatformTransactionManager and AOP, offering declarative and programmatic approaches, default rollback on runtime exceptions, customizable rollback rules, propagation behaviors, manual rollback techniques, multithreaded transaction handling, and solutions to common pitfalls such as swallowed exceptions and self-invocation issues.

Senior Xiao Ying
Senior Xiao Ying
Senior Xiao Ying
Mastering Spring Boot Transaction Rollback: A Complete Guide

Spring Framework’s transaction management relies on PlatformTransactionManager and follows AOP principles. Spring Boot auto‑configures the transaction manager, so adding the appropriate starter dependency enables transaction support.

Two Transaction Management Modes

Declarative transaction management : use the @Transactional annotation (recommended).

Programmatic transaction management : control transactions manually in code, offering higher flexibility but increased intrusion.

1. Transaction Rollback Mechanism

Spring’s default rollback rules are:

Rollback : automatic when a RuntimeException or Error occurs.

No rollback : CheckedException does not trigger rollback unless explicitly specified.

2. Declarative Transaction Rollback Configuration

Basic configuration and usage

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    // Default: rollback only on runtime exceptions
    @Transactional
    public void createUserDefault(String username) {
        userRepository.save(new User(username));
        // Runtime exception triggers rollback
        if ("invalid".equals(username)) {
            throw new RuntimeException("Invalid username");
        }
    }
}

Custom rollback rules

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    // Roll back for any exception
    @Transactional(rollbackFor = Exception.class)
    public void processOrder(Order order) throws OrderException {
        try {
            orderRepository.save(order);
            if (order.getAmount() <= 0) {
                throw new OrderException("Order amount must be greater than 0"); // checked exception
            }
        } catch (DataAccessException e) {
            // Log then re‑throw to trigger rollback
            throw new OrderException("Data access error", e);
        }
    }

    // Do not roll back for BusinessWarningException
    @Transactional(noRollbackFor = {BusinessWarningException.class})
    public void updateOrder(Order order) {
        orderRepository.update(order);
        if (order.getStatus().equals("EXPIRED")) {
            throw new BusinessWarningException("Order expired – log only, no rollback");
        }
    }
}

Transaction propagation and partial rollback

Spring provides seven propagation behaviors; REQUIRED, REQUIRES_NEW, and NESTED support partial rollback.

@Service
public class BankingService {
    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private TransactionLogRepository logRepository;

    // REQUIRED (default): join current transaction or create a new one
    @Transactional(propagation = Propagation.REQUIRED)
    public void transferMoneyRequired(Long from, Long to, BigDecimal amount) {
        accountRepository.debit(from, amount);
        accountRepository.credit(to, amount);
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new RuntimeException("Amount cannot be negative");
        }
    }

    // REQUIRES_NEW: always start a new transaction, independent rollback
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logTransactionRequiresNew(TransactionLog log) {
        logRepository.save(log);
        if (log.getDetails() == null) {
            throw new RuntimeException("Log details cannot be null");
        }
    }

    // NESTED: creates a savepoint, allows partial rollback (requires DB support)
    @Transactional(propagation = Propagation.NESTED)
    public void partialOperationNested(Entity entity) {
        repository.save(entity);
        if (entity.isInvalid()) {
            throw new RuntimeException("Entity validation failed");
        }
    }
}

See the accompanying diagram for a visual comparison of propagation behaviors.

Propagation behavior diagram
Propagation behavior diagram

3. Advanced Rollback Techniques and Scenario Handling

Manual transaction rollback without throwing an exception

@Service
public class InventoryService {
    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void reserveProduct(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            // Manually mark rollback but do not throw
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return;
        }
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    }
}

Exception capture with manual rollback

@Service
@Transactional
public class OrderProcessingService {
    public void processBatchOrders(List<Order> orders) {
        for (Order order : orders) {
            try {
                processSingleOrder(order);
            } catch (OrderProcessingException e) {
                log.error("Order processing failed: {}", order.getId(), e);
                // Mark the current transaction for rollback
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                // Optionally continue with remaining orders
            }
        }
    }

    private void processSingleOrder(Order order) throws OrderProcessingException {
        if (order.isInvalid()) {
            throw new OrderProcessingException("Invalid order");
        }
        // order processing logic …
    }
}

Transaction handling in multithreaded environments

In multithreaded scenarios, the @Transactional annotation does not apply automatically; transactions must be managed programmatically.

@Service
@Slf4j
public class BatchImportService {
    @Autowired
    private DataSourceTransactionManager transactionManager;
    @Autowired
    private UserRepository userRepository;
    @Value("${thread-pool.core-size:16}")
    private Integer corePoolSize;

    public void batchImportUsersConcurrent(List<User> users) {
        List<List<User>> partitions = Lists.partition(users, 100);
        CyclicBarrier barrier = new CyclicBarrier(partitions.size());
        AtomicBoolean hasError = new AtomicBoolean(false);
        ExecutorService executor = Executors.newFixedThreadPool(corePoolSize);

        List<CompletableFuture<Void>> futures = partitions.stream()
            .map(partition -> CompletableFuture.runAsync(() -> {
                DefaultTransactionDefinition def = new DefaultTransactionDefinition();
                def.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
                TransactionStatus status = transactionManager.getTransaction(def);
                try {
                    if (!hasError.get()) {
                        userRepository.saveAll(partition);
                        if (partition.stream().anyMatch(u -> u.getEmail() == null)) {
                            throw new RuntimeException("Email cannot be null");
                        }
                    }
                } catch (Exception e) {
                    hasError.set(true);
                    log.error("Partition processing failed", e);
                }
                try {
                    barrier.await();
                } catch (Exception e) {
                    transactionManager.rollback(status);
                    return;
                }
                if (hasError.get()) {
                    transactionManager.rollback(status);
                } else {
                    transactionManager.commit(status);
                }
            }, executor))
            .collect(Collectors.toList());

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        executor.shutdown();
    }
}

4. Common Issues and Solutions

1. Transaction does not roll back when the exception is swallowed

// ❌ Incorrect: exception caught, transaction commits
@Transactional
public void problematicMethod() {
    try {
        repository.save(entity);
        throw new RuntimeException("Business error");
    } catch (Exception e) {
        log.error("Error", e); // transaction will commit
    }
}

// ✅ Correct: re‑throw or manually mark rollback
@Transactional
public void correctMethod() {
    try {
        repository.save(entity);
        throw new RuntimeException("Business error");
    } catch (Exception e) {
        log.error("Error", e);
        // Option 1: re‑throw
        throw new RuntimeException("Wrapped exception", e);
        // Option 2: manual rollback
        // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

2. @Transactional on non‑public methods has no effect

// ❌ Not effective: private method
@Transactional
private void internalMethod() {
    // transaction does not apply
}

// ✅ Effective: public method
@Transactional
public void publicMethod() {
    // transaction works
}

3. Self‑invocation prevents proxy‑based transaction activation

@Service
public class SelfInvocationService {
    public void methodA() {
        methodB(); // self‑invocation, @Transactional ignored
    }

    @Transactional
    public void methodB() {
        // transaction not active
    }

    // ✅ Solution: obtain proxy from ApplicationContext
    @Autowired
    private ApplicationContext applicationContext;

    public void correctMethodA() {
        applicationContext.getBean(SelfInvocationService.class).methodB();
    }
}

Spring Boot’s transaction rollback mechanism provides flexible ways to ensure data consistency. Key takeaways include understanding the default behavior, configuring rollback attributes such as rollbackFor and propagation, handling exceptions properly, and managing complex scenarios like multithreaded processing.

Summary diagram
Summary diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootTransaction ManagementRollbackpropagationDeclarative TransactionsProgrammatic Transactions
Senior Xiao Ying
Written by

Senior Xiao Ying

Dedicated to sharing Java backend technical experience and original tutorials, offering career transition advice and resume editing. Recognized as a rising star in CSDN's Java backend community and ranked Top 3 in the 2022 New Star Program for Java backend.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.