Imagine this scenario: you need to implement a money transfer between two bank accounts. Obviously, the operation consists of two steps:
- Debit funds from one account.
- Credit funds to the other account.
If something goes wrong during the process — for example, a failure on the second step — it's important to roll back the whole operation to avoid a situation where the money "disappears". This is where the @Transactional annotation comes into play, letting you focus on business logic while Spring handles transaction management, making the code cleaner and simpler.
How does @Transactional work?
@Transactional is an annotation that defines transaction behavior for a method or class. Spring uses aspect-oriented programming (AOP) to wrap your code in "transactional proxies". That means when a method runs, Spring automatically starts a transaction, and when the method finishes — it either commits the changes (commit) or rolls them back (rollback) if an exception occurred.
Here's an example of using @Transactional for a money transfer between accounts:
@Service
public class BankService {
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
// Step 1: Debit funds from one account
accountRepository.debit(fromAccountId, amount);
// Step 2: Credit funds to the other account
accountRepository.credit(toAccountId, amount);
}
}
Simple? But that's just the tip of the iceberg.
Options on @Transactional
The @Transactional annotation provides a bunch of options to control transaction behavior. Let's go over the most important ones.
1. Propagation (how the transaction is propagated)
This specifies how a transaction should behave if the method is called inside another transaction. For example:
REQUIRED(default): Uses the existing transaction if there is one. If not, creates a new one.REQUIRES_NEW: Always creates a new transaction, suspending the current one.MANDATORY: Requires an existing transaction. If none exists — it will throw an exception.SUPPORTS: Runs within a transaction if one exists, but can be called without one.
Example:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
auditLogRepository.save(log);
}
2. Isolation (isolation level)
Specifies how a transaction should see changes made by other transactions. This matters in multi-user systems.
Here are the main isolation levels:
READ_UNCOMMITTED: a transaction can see uncommitted changes from other transactions (dirty reads).READ_COMMITTED: prevents dirty reads.REPEATABLE_READ: protection against non-repeatable reads (consistent reads).SERIALIZABLE: full isolation, maximum protection from conflicts, but with a performance hit.
Example:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void performSensitiveOperation() {
// Mission-critical operation
}
3. Timeout (max duration)
Sometimes transactions can get stuck or run too long. You can set a time limit (in seconds), after which the transaction will be aborted.
Example:
@Transactional(timeout = 5) // Max 5 seconds
public void executeLongQuery() {
// Long operation
}
4. ReadOnly (read-only)
If the transaction only reads data (no changes), you can set readOnly=true. This can optimize performance.
Example:
@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
return accountRepository.findAll();
}
5. RollbackFor (rollback on error)
By default Spring rolls back a transaction only for RuntimeException or Error. If you want to roll back on other exceptions (for example, Exception), you need to specify it explicitly:
@Transactional(rollbackFor = Exception.class)
public void processTransaction() throws Exception {
// Code that might throw an exception
}
Real-world examples
Example 1: Order processing in an online store
Imagine processing an order that includes:
- Decreasing product stock
- Charging the customer's card
- Recording the order data
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// Check product availability
stockService.decreaseStock(order.getProductId(), order.getQuantity());
// Charge payment
paymentService.charge(order.getPaymentInfo());
// Save order
orderRepository.save(order);
}
}
If any step fails, all changes will be rolled back.
Example 2: Error logging
Sometimes you want to save an event log outside the main transaction. Use REQUIRES_NEW:
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logError(String message) {
errorLogRepository.save(new ErrorLog(message));
}
}
Common mistakes when working with @Transactional
- The annotation didn't take effect. This can happen if the method is called from the same class (via
this). Remember Spring uses proxies, and internal calls inside the same class bypass AOP. Solution? Move the method to another@Service. - Mixing external and internal transactions. For example, using
@Transactional(propagation = Propagation.REQUIRES_NEW)inside a method can introduce overhead. Use it only when necessary. - Incorrect rollback behavior. If you forgot to specify
rollbackFor, Spring might not roll back the transaction. Always explicitly declare the behavior you need.
Practice: Implementing @Transactional in an application
Let's create a method to process an order with automatic rollback on error:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void placeOrder(Long productId, int quantity) {
// Decrease product stock
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found!"));
if (product.getStock() < quantity) {
throw new RuntimeException("Not enough stock available!");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// Save the order
Order order = new Order();
order.setProductId(productId);
order.setQuantity(quantity);
orderRepository.save(order);
// Artificial failure to test rollback
if (quantity > 10) {
throw new RuntimeException("Failure while processing the order!");
}
}
}
Check:
- Calling
placeOrderwith valid data — will create the order and reduce the product stock. - Running with a large quantity (
quantity > 10) — will trigger a failure. Spring will roll back the changes, keeping the data consistent.
@Transactional is a powerful tool that saves you the headache of manual transaction management. Use it responsibly, tune the parameters, and always validate behavior with tests!
GO TO FULL VERSION