Understanding @Transactional in Spring Boot
@Transactional is a Spring annotation that manages database transactions automatically. It ensures:
- A database connection stays open for the duration of the method
- Multiple database operations are grouped together (all succeed or all fail)
- Lazy-loaded data can be accessed within the transaction boundary
@Transactional
public void someMethod() {
// Database operations here
// Transaction stays open until method completes
}
The Phone Call Analogy
Think of your app talking to a database like making a phone call.
Without @Transactional — you hang up too early
┌─────────────┐ ┌──────────────┐
│ Service │ │ Database │
└─────────────┘ └──────────────┘
│ │
│ "Save this order" │
│───────────────────────────────────>│
│ "Done!" │
│<───────────────────────────────────│
│ *hangs up* ❌ Phone disconnected │
┌─────────────┐
│ Controller │
└─────────────┘
│ "What were the details?"
│──────────> Service
│ 💥 Error! Can't call database - phone is disconnected!
With @Transactional — the line stays open
┌─────────────┐ ┌──────────────┐
│ Service │ │ Database │
└─────────────┘ └──────────────┘
│ *picks up phone* │
│ @Transactional starts │
│ │
│ "Save this order" │
│───────────────────────────────────>│
│ "Done!" │
│<───────────────────────────────────│
│ *keeps phone connected* ✓ │
┌─────────────┐
│ Controller │
└─────────────┘
│ "What were the details?"
│──────────> Service
│ "Database, send the details"
│───────────────────────────────────>│
│ *sends details* ✓ │
│<───────────────────────────────────│
│ *hangs up* │
│ @Transactional ends │
Why Do We Need It?
Problem 1: LazyInitializationException
The most common pain point. Lazy collections need an open session to load.
@Entity
public class Order {
@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;
}
❌ Without @Transactional:
public Order create(OrderDTO dto) {
Order order = new Order();
return repository.save(order);
// Session closes here!
}
// Controller accesses order.getItems() → 💥 LazyInitializationException
✅ With @Transactional:
@Transactional
public Order create(OrderDTO dto) {
Order order = new Order();
return repository.save(order);
// Session stays open — lazy collections accessible
}
Problem 2: Atomicity (All or Nothing)
// ❌ Without @Transactional
public void transferMoney(Account from, Account to, double amount) {
from.withdraw(amount); // succeeds ✓
to.deposit(amount); // fails 💥 → money disappears! 💸
}
// ✅ With @Transactional
@Transactional
public void transferMoney(Account from, Account to, double amount) {
from.withdraw(amount);
to.deposit(amount);
// If deposit fails → withdrawal is rolled back automatically
}
Problem 3: Dirty Updates Getting Lost
// ❌ Without @Transactional — change is lost
public void updateStatus(Long id) {
Order order = repository.findById(id).get();
order.setStatus("PROCESSING");
// No transaction → change discarded silently
}
// ✅ With @Transactional — Hibernate auto-saves at transaction end
@Transactional
public void updateStatus(Long id) {
Order order = repository.findById(id).get();
order.setStatus("PROCESSING");
// Automatically persisted ✓
}
How Hibernate Sessions Work
Order order = new Order(); → TRANSIENT (not in DB)
repository.save(order); → PERSISTENT (tracked by Hibernate, session open)
// @Transactional method ends → DETACHED (in memory, session closed ❌)
Without @Transactional
public Order save(Order order) {
┌─────────────────────────────┐
│ Hibernate Session OPENS │ ← repository.save() opens session
└─────────────────────────────┘
Order saved = repository.save(order);
┌─────────────────────────────┐
│ Hibernate Session CLOSES │ ← save() method ends
└─────────────────────────────┘
return saved; // ← Entity is now DETACHED ❌
}
With @Transactional
@Transactional
public Order save(Order order) {
┌─────────────────────────────┐
│ Hibernate Session OPENS │ ← @Transactional opens session
└─────────────────────────────┘
Order saved = repository.save(order);
// Session stays OPEN ✓
return saved; // ← Entity is still MANAGED ✓
┌─────────────────────────────┐
│ Hibernate Session CLOSES │ ← After method fully completes
└─────────────────────────────┘
}
Once an entity is detached, accessing lazy collections throws an exception. @Transactional keeps it in the persistent state until you're done with it.
Best Practices
1. Put @Transactional on the Service Layer
// ✅ Good
@Service
public class OrderService {
@Transactional
public Order create(OrderDTO dto) { ... }
}
// ❌ Bad — controllers should be thin
@RestController
public class OrderController {
@Transactional // Don't do this!
public ResponseEntity<?> createOrder() { ... }
}
2. Use readOnly = true for Query Methods
@Transactional(readOnly = true)
public List<Order> findAll() {
return repository.findAll();
}
Better performance + prevents accidental writes.
3. Convert to DTO Inside the Transaction
// ✅ Best — lazy data is still accessible during conversion
@Transactional
public OrderResponseDTO create(OrderDTO dto) {
Order saved = repository.save(OrderConverter.toEntity(dto));
return OrderConverter.toResponseDTO(saved); // convert here!
}
4. Specify Rollback Rules When Needed
@Transactional(rollbackFor = Exception.class)
public void processPayment() { ... }
@Transactional(noRollbackFor = AuditException.class)
public void logActivity() { ... }
5. Be Careful with Transaction Propagation
@Transactional
public void methodA() {
methodB(); // shares same transaction by default
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// runs in its own transaction
// if this fails, methodA is NOT rolled back
}
Quick Fixes for LazyInitializationException
If you can't add @Transactional, you have alternatives:
Option 1: Change to EAGER loading (use carefully — can hurt performance)
@OneToMany(fetch = FetchType.EAGER)
private List<OrderItem> items;
Option 2: Force initialization while session is open
saved.getItems().size(); // triggers lazy load
Option 3: Fetch join query
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
Summary
| Situation | Use @Transactional? |
|---|---|
| Service method with DB operations | ✅ Yes |
| Accessing lazy-loaded collections | ✅ Yes |
| Multiple operations that must be atomic | ✅ Yes |
| Controller methods | ❌ No |
| Pure read with no lazy access | 🟡 Optional (readOnly = true) |
💡 The golden rule: If your method touches the database and returns entities whose lazy relationships will be accessed later — use
@Transactional.
Think of it as keeping the phone line to the database open. Without it, you're asking questions after you've already hung up! 📞
Top comments (0)