A Spring transaction groups database operations into one unit through a transaction manager and proxy interception. Put @Transactional on a public service operation that represents the business unit of work, not on controllers or every repository method by habit.
Spring's @Transactional annotation manages database transactions declaratively. When applied to a method or class, Spring wraps the execution in a transaction - committing on success and rolling back on unchecked exceptions (RuntimeException and Error) by default.
Add @EnableTransactionManagement to your configuration class (Spring Boot enables it automatically).
package com.example.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final AuditService auditService;
public OrderService(OrderRepository orderRepository,
InventoryService inventoryService,
AuditService auditService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.auditService = auditService;
}
// Default: REQUIRED propagation, DEFAULT isolation, rollback on RuntimeException
@Transactional
public Order placeOrder(Long userId, Long productId, int qty) {
// All operations share the same transaction
inventoryService.deductStock(productId, qty); // participates in this transaction
Order order = orderRepository.save(new Order(userId, productId, qty));
auditService.logOrder(order); // also in same transaction
return order;
// Commits here if no exception
}
// Read-only: hint to DB to optimize (no dirty checking, no flush)
@Transactional(readOnly = true)
public List<Order> getOrdersByUser(Long userId) {
return orderRepository.findByUserId(userId);
}
// Rollback on checked exceptions too
@Transactional(rollbackFor = Exception.class)
public void processPayment(Long orderId) throws Exception {
// Rolls back even if a checked Exception is thrown
}
// Never rollback on IllegalArgumentException
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void updateOrder(Long orderId, String status) {
// IllegalArgumentException won't trigger rollback
}
// SERIALIZABLE isolation: highest level, prevents phantom reads
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
// Fully isolated from other transactions
}
// REQUIRES_NEW: always starts a new transaction, suspends the current one
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditAlways(String message) {
// This runs in its own transaction - commits even if outer tx rolls back
auditService.save(message);
}
}
| Propagation | Behavior |
|---|---|
| REQUIRED (default) | Join existing transaction; create new one if none exists |
| REQUIRES_NEW | Always create a new transaction; suspend existing one |
| SUPPORTS | Join existing transaction; run non-transactionally if none |
| NOT_SUPPORTED | Run non-transactionally; suspend existing transaction |
| MANDATORY | Must run within an existing transaction; throw exception if none |
| NEVER | Must NOT run within a transaction; throw exception if one exists |
| NESTED | Run within a nested transaction (savepoint); rollback to savepoint on failure |
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ_UNCOMMITTED | Possible | Possible | Possible |
| READ_COMMITTED | Prevented | Possible | Possible |
| REPEATABLE_READ | Prevented | Prevented | Possible |
| SERIALIZABLE | Prevented | Prevented | Prevented |
package com.example.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class ProgrammaticTxService {
private final TransactionTemplate transactionTemplate;
private final OrderRepository orderRepository;
public ProgrammaticTxService(PlatformTransactionManager txManager,
OrderRepository orderRepository) {
// TransactionTemplate wraps PlatformTransactionManager
this.transactionTemplate = new TransactionTemplate(txManager);
this.orderRepository = orderRepository;
}
public Order createOrderProgrammatically(Order order) {
// Configure transaction settings
transactionTemplate.setIsolationLevel(
TransactionTemplate.ISOLATION_READ_COMMITTED);
transactionTemplate.setTimeout(30); // seconds
// execute() returns a value; executeWithoutResult() for void
return transactionTemplate.execute(status -> {
try {
Order saved = orderRepository.save(order);
// ... more operations
return saved;
} catch (Exception e) {
// Manually mark for rollback
status.setRollbackOnly();
throw new RuntimeException("Order creation failed", e);
}
});
}
}
@Service
public class TransactionPitfalls {
// PITFALL 1: Self-invocation bypasses the proxy - @Transactional has no effect
public void outerMethod() {
this.innerMethod(); // Calls directly, NOT through Spring proxy - NO transaction!
}
@Transactional
public void innerMethod() {
// This transaction is IGNORED when called from outerMethod() above
}
// FIX: Inject self or use a separate bean
@Autowired
private TransactionPitfalls self;
public void outerMethodFixed() {
self.innerMethod(); // Goes through proxy - transaction works!
}
// PITFALL 2: @Transactional on private methods - Spring proxy can't intercept
@Transactional
private void privateMethod() {
// NO transaction - Spring AOP only works on public methods
}
// PITFALL 3: Catching RuntimeException prevents rollback
@Transactional
public void catchingException() {
try {
// ... DB operation
throw new RuntimeException("error");
} catch (RuntimeException e) {
// Swallowing the exception - transaction COMMITS instead of rolling back!
System.out.println("Caught: " + e.getMessage());
}
}
// FIX: Re-throw or mark rollback manually
@Transactional
public void catchAndRethrow(TransactionStatus status) {
try {
throw new RuntimeException("error");
} catch (RuntimeException e) {
// Option 1: re-throw
throw e;
// Option 2: mark rollback only (if you need to handle the exception)
// TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
By default, declarative transactions roll back for RuntimeException and Error; checked-exception behavior must be configured when the application contract requires rollback. Do not catch an exception inside the transaction and return success unless the transaction’s state is known and intentionally committed.
Self-invocation bypasses the proxy, so calling one @Transactional method from another method on the same object does not start a new intercepted boundary. Keep transactions short, avoid remote calls while locks are held, and select propagation and isolation only for a demonstrated consistency requirement.
Explore 500+ free tutorials across 20+ languages and frameworks.