In Spring, transactions are usually managed with the @Transactional annotation.
A transaction makes sure that a group of database operations is either:
- all succeed, or
- all fail and roll back
This is important when one business operation changes multiple records or tables.
1. Enable Transaction Management
If you are using Spring Boot with Spring Data JPA, transaction management is usually configured automatically.
In most Spring Boot applications, you do not need to manually enable it.
If you are using plain Spring configuration, you may need:
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
}
With Spring Boot, this is normally unnecessary.
2. Use @Transactional on Service Methods
The most common place to put transactions is the service layer, not the controller or repository.
Example:
package com.example.app.order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
public OrderService(
OrderRepository orderRepository,
PaymentRepository paymentRepository
) {
this.orderRepository = orderRepository;
this.paymentRepository = paymentRepository;
}
@Transactional
public void placeOrder(Order order, Payment payment) {
orderRepository.save(order);
paymentRepository.save(payment);
}
}
If paymentRepository.save(payment) fails, Spring rolls back the earlier orderRepository.save(order) operation.
3. Use readOnly = true for Query Methods
For methods that only read data, use:
@Transactional(readOnly = true)
Example:
package com.example.app.order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class OrderQueryService {
private final OrderRepository orderRepository;
public OrderQueryService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional(readOnly = true)
public List<Order> findAllOrders() {
return orderRepository.findAll();
}
}
readOnly = true can help performance and communicates that the method should not modify data.
4. Rollback Behavior
By default, Spring rolls back a transaction for:
RuntimeExceptionError
By default, Spring does not roll back for checked exceptions.
Example:
@Transactional
public void updateOrder() {
throw new IllegalStateException("Something failed");
}
This transaction rolls back because IllegalStateException is a runtime exception.
5. Roll Back for Checked Exceptions
If you want rollback for a checked exception, specify rollbackFor.
@Transactional(rollbackFor = Exception.class)
public void importOrders() throws Exception {
// database changes
throw new Exception("Import failed");
}
You can also target a specific exception:
@Transactional(rollbackFor = OrderImportException.class)
public void importOrders() throws OrderImportException {
// database changes
throw new OrderImportException("Import failed");
}
6. Avoid Catching Exceptions Without Rethrowing
This can prevent rollback:
@Transactional
public void placeOrder(Order order) {
try {
orderRepository.save(order);
paymentService.charge(order);
} catch (Exception ex) {
// Bad if you swallow the exception
}
}
If the exception is caught and not rethrown, Spring may think the method completed successfully and commit the transaction.
Prefer:
@Transactional
public void placeOrder(Order order) {
try {
orderRepository.save(order);
paymentService.charge(order);
} catch (Exception ex) {
throw new OrderProcessingException("Could not place order", ex);
}
}
7. Transaction Boundaries Should Match Business Operations
A transaction should usually wrap one complete business action.
Good examples:
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
debitAccount(fromAccountId, amount);
creditAccount(toAccountId, amount);
}
@Transactional
public void registerUser(RegisterUserRequest request) {
createUser(request);
createDefaultSettings(request);
sendWelcomeEvent(request);
}
Avoid making transactions too large, especially if they include slow external calls.
8. Be Careful with External API Calls
Avoid doing slow network calls inside a database transaction when possible.
Less ideal:
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
paymentGateway.charge(order); // external network call inside transaction
order.setStatus(OrderStatus.PAID);
}
Better pattern:
@Transactional
public Order createPendingOrder(Order order) {
order.setStatus(OrderStatus.PENDING_PAYMENT);
return orderRepository.save(order);
}
@Transactional
public void markOrderPaid(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
order.setStatus(OrderStatus.PAID);
}
Then call the payment gateway between those operations.
9. Common @Transactional Options
@Transactional(
readOnly = false,
rollbackFor = Exception.class,
timeout = 10
)
public void processOrder() {
// database work
}
Common options:
| Option | Meaning |
|---|---|
readOnly |
Marks the transaction as read-only |
rollbackFor |
Exceptions that should trigger rollback |
noRollbackFor |
Exceptions that should not trigger rollback |
timeout |
Maximum transaction duration in seconds |
propagation |
How this method joins or creates transactions |
isolation |
How isolated this transaction is from other transactions |
10. Propagation Basics
Propagation controls what happens if a transactional method calls another transactional method.
The default is:
Propagation.REQUIRED
That means:
- join the existing transaction if one exists
- otherwise create a new transaction
Example:
@Transactional
public void checkout() {
reserveInventory();
chargePayment();
}
If reserveInventory() and chargePayment() are also transactional with default propagation, they participate in the same transaction.
Common propagation values:
| Propagation | Meaning |
|---|---|
REQUIRED |
Use current transaction or create one |
REQUIRES_NEW |
Always start a new transaction |
MANDATORY |
Must already have a transaction |
SUPPORTS |
Use a transaction if one exists |
NOT_SUPPORTED |
Run without a transaction |
NEVER |
Fail if a transaction exists |
NESTED |
Use a nested transaction if supported |
Example using a separate transaction for audit logging:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(String message) {
auditLogRepository.save(new AuditLog(message));
}
11. Isolation Basics
Isolation controls how much one transaction can see changes from another transaction.
Example:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processPayment() {
// database work
}
Common isolation levels:
| Isolation | Meaning |
|---|---|
DEFAULT |
Use the database default |
READ_UNCOMMITTED |
May read uncommitted changes |
READ_COMMITTED |
Only read committed data |
REPEATABLE_READ |
Same row read twice stays consistent |
SERIALIZABLE |
Strongest isolation, lowest concurrency |
Most applications use the database by default unless there is a specific consistency problem.
12. Important Limitation: Self-Invocation
Spring transactions are usually applied through proxies.
That means this may not start a transaction as expected:
@Service
public class UserService {
public void outerMethod() {
innerMethod();
}
@Transactional
public void innerMethod() {
// database work
}
}
Because innerMethod() is called from the same class, the call may bypass Spring’s transactional proxy.
Prefer calling transactional methods from another Spring bean, or put @Transactional on the outer method:
@Service
public class UserService {
@Transactional
public void outerMethod() {
innerMethod();
}
public void innerMethod() {
// database work
}
}
13. Recommended Structure
A typical Spring application uses transactions like this:
Controller
↓
Service ← @Transactional here
↓
Repository
↓
Database
Example:
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/orders")
public void createOrder(@RequestBody Order order) {
orderService.createOrder(order);
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
}
}
Quick Rules
Use these defaults for most Spring applications:
- Put
@Transactionalon service methods. - Use
@Transactional(readOnly = true)for query methods. - Use
@Transactionalfor create, update, and delete methods. - Do not swallow exceptions inside transactional methods.
- Use
rollbackForif you need rollback for checked exceptions. - Keep transactions short.
- Avoid slow external API calls inside transactions.
- Be aware that self-invocation can bypass transactional behavior.
For most Spring Boot + Spring Data JPA applications, this is enough:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public List<User> findUsers() {
return userRepository.findAll();
}
@Transactional
public User createUser(User user) {
return userRepository.save(user);
}
}
