In the previous lecture we covered exception types in transactions and the basic mechanisms for handling them in Spring. Now it's time to dig into practical use and go over more complex rollback scenarios.
Why is it important to configure rollback correctly?
Misconfigured rollbacks can lead to serious issues:
- Incomplete changes: some data may remain in an inconsistent state, for example when transferring money between accounts.
- Violation of business rules: the system may persist changes that contradict business logic (like an order exceeding the limit).
- Scaling problems: in multi-user systems incorrect transaction handling leads to hard-to-track bugs.
Spring provides flexible tools to control transaction rollbacks. Let's learn how to use them.
Practical rollback usage scenarios
In real apps you often run into cases where the default transaction behavior isn't enough. Let's look at an online store example to see how to flexibly manage transactions in different situations.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductService productService;
@Transactional
public void processOrder(Order order) {
// Check product availability
if (!productService.isAvailable(order.getProductId())) {
// Programmatic rollback: explicitly tell Spring to roll back the transaction
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new ProductNotAvailableException("Product not available");
}
// Save the order
orderRepository.save(order);
// Check user limits
if (orderRepository.getUserOrderCount(order.getUserId()) > 5) {
// Automatic rollback: Spring will roll back the transaction on exception
throw new BusinessException("Order limit exceeded");
}
// Decrease stock and handle special cases
try {
productService.decreaseStock(order.getProductId(), order.getQuantity());
} catch (StockWarningException e) {
// Log the warning but allow the transaction to complete
notifyManager(e);
}
}
// Example where some errors shouldn't cancel the whole operation
@Transactional(noRollbackFor = {StockWarningException.class})
public void processOrderWithPartialErrors(Order order) {
orderRepository.save(order);
productService.decreaseStock(order.getProductId(), order.getQuantity());
// Even if a StockWarningException occurs,
// the order will remain saved
}
}
In this example we see three different approaches to transaction control:
- Programmatic rollback via
setRollbackOnly()— when we need full control over the process - Automatic rollback on exception — Spring's default behavior for RuntimeException
- Selective rollback using
noRollbackFor— when some errors shouldn't cancel the whole operation
Each approach has its advantages:
- Programmatic rollback gives maximum control
- Automatic rollback keeps the code cleaner and easier to read
- Selective rollback lets you flexibly handle different types of errors
Testing rollback
To verify transaction behavior, Spring provides handy testing tools. Let's see how we can test our order handling logic:
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
@Transactional
public void testOrderRollbackOnLimitExceeded() {
// Setup
Order order = new Order("Product A", 1);
// Action
assertThrows(BusinessException.class, () -> {
orderService.processOrder(order);
});
// Assertion
assertEquals(0, orderRepository.count(),
"Database should be empty after transaction rollback");
}
@Test
@Transactional
public void testPartialRollback() {
Order order = new Order("Product B", 1);
// This method does not roll back the transaction for StockWarningException
orderService.processOrderWithPartialErrors(order);
// Check that the order was saved despite possible warnings
assertTrue(orderRepository.existsById(order.getId()));
}
}
Note:
- The
@Transactionalannotation in tests automatically rolls back all changes after each test - This lets us avoid worrying about cleaning the database between tests
- We can easily verify how rollback works in different scenarios
Common mistakes when working with rollback
Working with rollback can be a blessing or a source of problems if you're not careful:
1. Catching exceptions manually: If you catch an exception inside a transactional method, Spring won't know about the error and the transaction will commit successfully.
Example:
@Transactional
public void incorrectTransactionHandling() {
try {
saveDataToDatabase();
throw new RuntimeException("Error");
} catch (Exception e) {
// Exception was swallowed, transaction won't roll back!
}
}
2. Calling a transactional method via this: Spring uses proxies to manage transactions. If you call a transactional method from another method in the same class via this, the @Transactional annotation is ignored.
public class OrderService {
@Transactional
public void methodA() {
// Transaction will work
}
public void methodB() {
this.methodA(); // Transaction won't work
}
}
Solution: inject the service into itself or move the call to another class.
3. Operations outside a transaction: if you change data outside a transaction, it won't be rolled back. For example, calling an external API or modifying static variables are outside the transaction scope.
Conclusion
Transaction rollback is a powerful mechanism in Spring that helps keep data consistent in case of failures or business errors. But like any tool, rollbacks should be used thoughtfully. I hope you won't be afraid of the word rollback anymore — it's not a bug, it's a lifeline when something goes wrong.
GO TO FULL VERSION