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.
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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
