Understanding @Transactional in Spring Boot
Table of Contents
- What is @Transactional?
- The Phone Call Analogy
- Why Do We Need It?
- How Hibernate Sessions Work
- Common Problems Without @Transactional
- Real-World Example
- Best Practices
- Advanced Concepts
What is @Transactional?
@Transactional is a Spring annotation that manages database transactions automatically. It ensures that:
- 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
Basic Syntax
@Transactional
public void someMethod() {
// Database operations here
// Transaction stays open until method completes
}
The Phone Call Analogy
Imagine your application needs to talk to a database. Think of it like making a phone call:
Without @Transactional (Short Call)
┌─────────────┐ ┌──────────────┐
│ Service │ │ Database │
└─────────────┘ └──────────────┘
│ │
│ "Save this rule" │
│───────────────────────────────────>│
│ │
│ "Done!" │
│<───────────────────────────────────│
│ │
│ *hangs up* │
│ ❌ Phone disconnected │
│ │
┌─────────────┐
│ Controller │
└─────────────┘
│
│ "What were the details?"
│──────────> Service
│
│ "Let me check..."
│ 💥 Error! Can't call database - phone is disconnected!
With @Transactional (Extended Call)
┌─────────────┐ ┌──────────────┐
│ Service │ │ Database │
└─────────────┘ └──────────────┘
│ │
│ *picks up phone* │
│ @Transactional starts │
│ │
│ "Save this rule" │
│───────────────────────────────────>│
│ │
│ "Done!" │
│<───────────────────────────────────│
│ │
│ *keeps phone connected* │
│ ✓ Still on call │
│ │
┌─────────────┐
│ Controller │
└─────────────┘
│
│ "What were the details?"
│──────────> Service
│
│ "Database, send the details"
│───────────────────────────────────>│
│ │
│ *sends details* │
│<───────────────────────────────────│
│ │
│ *hangs up* │
│ @Transactional ends │
Why Do We Need It?
Problem 1: LazyInitializationException
This is the most common problem when @Transactional is missing.
@Entity
public class Rule {
@ElementCollection(fetch = FetchType.LAZY) // ← Not loaded immediately
private Set<ServiceDefinition> serviceDefinitions;
}
Without @Transactional:
public Rule create(RuleRequestDTO dto) {
Rule rule = new Rule();
return repository.save(rule);
// ← Session closes here!
}
// Later in controller:
rule.getServiceDefinitions(); // 💥 LazyInitializationException!
With @Transactional:
@Transactional // ← Session stays open
public Rule create(RuleRequestDTO dto) {
Rule rule = new Rule();
return repository.save(rule);
}
// Later in controller:
rule.getServiceDefinitions(); // ✓ Works! Session is still open
Problem 2: Multiple Operations Need to be Atomic
@Transactional // ← All or nothing!
public void transferMoney(Account from, Account to, double amount) {
from.withdraw(amount); // Operation 1
to.deposit(amount); // Operation 2
// If Operation 2 fails, Operation 1 is rolled back automatically
}
Without @Transactional, if Operation 2 fails:
- Money is withdrawn from
fromaccount ✓ - Money is NOT deposited to
toaccount ❌ - Money disappears! 💸
Problem 3: Data Consistency
@Transactional
public void updateOrderStatus(Long orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus("PROCESSING");
// No one else can modify this order until transaction completes
// Ensures data consistency
orderRepository.save(order);
}
How Hibernate Sessions Work
The Lifecycle of an Entity
┌──────────────────────────────────────────────────────────┐
│ Entity Lifecycle │
└──────────────────────────────────────────────────────────┘
1. TRANSIENT (New)
↓
Rule rule = new Rule(); // Just created, not in database
2. PERSISTENT (Managed)
↓
repository.save(rule); // Now tracked by Hibernate
│
├─ Hibernate Session is OPEN
├─ Can access lazy collections
├─ Changes are automatically saved
│
3. DETACHED (Disconnected)
↓
// @Transactional method ends
// Session CLOSES
│
├─ Entity still exists in memory
├─ But NOT connected to database
├─ ❌ Cannot access lazy collections
├─ Changes are NOT automatically saved
Visualizing the Session
// Without @Transactional
public Rule save(Rule rule) {
┌─────────────────────────────┐
│ Hibernate Session OPENS │ ← repository.save() opens session
└─────────────────────────────┘
Rule saved = repository.save(rule);
┌─────────────────────────────┐
│ Hibernate Session CLOSES │ ← save() method ends
└─────────────────────────────┘
return saved; // ← Entity is now DETACHED
}
// With @Transactional
@Transactional
public Rule save(Rule rule) {
┌─────────────────────────────┐
│ Hibernate Session OPENS │ ← @Transactional opens session
└─────────────────────────────┘
Rule saved = repository.save(rule);
// Session stays OPEN
return saved; // ← Entity is still MANAGED
┌─────────────────────────────┐
│ Hibernate Session CLOSES │ ← After caller finishes
└─────────────────────────────┘
}
Common Problems Without @Transactional
Problem 1: LazyInitializationException
Scenario:
// Entity
@Entity
public class Rule {
@ElementCollection(fetch = FetchType.LAZY)
private Set<ServiceDefinition> serviceDefinitions;
}
// Service (No @Transactional)
public Rule create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
return repository.save(rule); // Session closes here
}
// Controller
public ResponseEntity<?> createRule(RuleRequestDTO dto) {
Rule rule = service.create(dto); // rule is detached
// Trying to access lazy collection
RuleResponseDTO response = RuleConverter.toResponseDTO(rule);
// ↑ This calls rule.getServiceDefinitions()
// 💥 org.hibernate.LazyInitializationException!
}
Error Message:
org.hibernate.LazyInitializationException:
failed to lazily initialize a collection of role:
com.example.Rule.serviceDefinitions,
could not initialize proxy - no Session
Solution:
@Transactional // ← Add this!
public Rule create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
return repository.save(rule);
}
Problem 2: Data Not Persisted
Scenario:
// Without @Transactional
public void updateRule(Long id, String newName) {
Rule rule = repository.findById(id).get();
rule.setName(newName);
// Where's the save()?
// In JPA, if entity is MANAGED, changes auto-save at transaction end
// But without @Transactional, there's no transaction!
}
Result: Changes are lost! 💾❌
Solution:
@Transactional // ← Now changes are automatically saved
public void updateRule(Long id, String newName) {
Rule rule = repository.findById(id).get();
rule.setName(newName);
// Automatic save at transaction end!
}
Problem 3: Inconsistent Data
Scenario:
// Without @Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId).get();
order.setStatus("PROCESSING");
orderRepository.save(order);
// If this fails ↓
paymentService.charge(order.getAmount());
// Order status is still "PROCESSING" even though payment failed!
}
Solution:
@Transactional // ← All or nothing!
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId).get();
order.setStatus("PROCESSING");
orderRepository.save(order);
paymentService.charge(order.getAmount());
// If this fails, EVERYTHING rolls back!
}
Real-World Example
Let's trace through your exact scenario:
Your Code Structure
// Entity
@Entity
public class Rule {
@Id
@GeneratedValue
private Long id;
@ElementCollection(fetch = FetchType.LAZY) // ← LAZY!
private Set<ServiceDefinition> serviceDefinitions;
}
// Service
public class RuleService {
public Rule create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
return ruleRepository.save(rule);
}
}
// Controller
@PostMapping("/rules")
public ResponseEntity<?> createRule(@RequestBody RuleRequestDTO dto) {
Rule rule = ruleService.create(dto); // ← Rule is returned
// Convert to response DTO
RuleResponseDTO response = RuleConverter.toResponseDTO(rule);
return ResponseEntity.ok(response);
}
// Converter
public class RuleConverter {
public static RuleResponseDTO toResponseDTO(Rule rule) {
RuleResponseDTO dto = new RuleResponseDTO();
dto.setId(rule.getId());
dto.setName(rule.getName());
// Accessing lazy collection!
dto.setServiceDefinitions(rule.getServiceDefinitions()); // 💥 Error!
return dto;
}
}
Execution Flow Without @Transactional
Step 1: Controller calls service.create()
↓
Step 2: Service creates entity and calls repository.save()
↓
┌──────────────────────────────────────┐
│ Hibernate Session OPENS │
│ - Inserts rule into database │
│ - Generates ID │
│ - Rule saved successfully ✓ │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ Hibernate Session CLOSES │
│ - Rule becomes DETACHED │
└──────────────────────────────────────┘
↓
Step 3: Service returns rule to controller
↓
Step 4: Controller calls RuleConverter.toResponseDTO(rule)
↓
Step 5: Converter tries to access rule.getServiceDefinitions()
↓
┌──────────────────────────────────────┐
│ 💥 LazyInitializationException │
│ │
│ "Session is closed!" │
│ "Cannot load lazy collection!" │
└──────────────────────────────────────┘
Execution Flow With @Transactional
Step 1: Controller calls service.create()
↓
┌──────────────────────────────────────┐
│ @Transactional starts │
│ Hibernate Session OPENS │
└──────────────────────────────────────┘
↓
Step 2: Service creates entity and calls repository.save()
↓
│ - Inserts rule into database │
│ - Generates ID │
│ - Rule saved successfully ✓ │
│ - Session STAYS OPEN │
↓
Step 3: Service returns rule to controller
│ - Rule is still MANAGED │
↓
Step 4: Controller calls RuleConverter.toResponseDTO(rule)
↓
Step 5: Converter accesses rule.getServiceDefinitions()
↓
│ - Session is STILL OPEN ✓ │
│ - Hibernate loads lazy collection │
│ - Returns serviceDefinitions │
↓
Step 6: Response is built
↓
┌──────────────────────────────────────┐
│ @Transactional ends │
│ Hibernate Session CLOSES │
└──────────────────────────────────────┘
↓
✓ Success!
Best Practices
1. Place @Transactional on Service Layer
// ✓ GOOD - Service layer
@Service
public class RuleService {
@Transactional
public Rule create(RuleRequestDTO dto) {
// Business logic here
}
}
// ❌ BAD - Controller layer
@RestController
public class RuleController {
@Transactional // Don't do this!
public ResponseEntity<?> createRule() {
// ...
}
}
Why? Service layer contains business logic. Controllers should be thin.
2. Read-Only Transactions for Query Methods
@Transactional(readOnly = true) // ← Optimizes performance
public List<Rule> findAll() {
return repository.findAll();
}
Benefits:
- Better performance
- Prevents accidental modifications
- Database can optimize read-only queries
3. Specify Rollback Conditions
@Transactional(rollbackFor = Exception.class) // Roll back on any exception
public void processPayment() {
// If ANY exception occurs, roll back
}
@Transactional(noRollbackFor = CustomException.class) // Don't roll back for this
public void logActivity() {
// Even if CustomException occurs, keep the transaction
}
4. Convert to DTO Inside Transaction
// ✓ BEST PRACTICE
@Transactional
public RuleResponseDTO create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
Rule saved = repository.save(rule);
return RuleConverter.toResponseDTO(saved); // Convert here!
}
// ❌ RISKY
@Transactional
public Rule create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
return repository.save(rule); // Return entity - can cause lazy loading issues
}
5. Be Careful with Transaction Propagation
@Transactional // ← Creates new transaction
public void methodA() {
methodB(); // Uses same transaction by default
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // ← Forces new transaction
public void methodB() {
// Runs in separate transaction
// If this fails, methodA is not affected
}
Advanced Concepts
Transaction Isolation Levels
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney() {
// Other transactions cannot see uncommitted changes
}
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ_UNCOMMITTED | ✓ | ✓ | ✓ |
| READ_COMMITTED | ❌ | ✓ | ✓ |
| REPEATABLE_READ | ❌ | ❌ | ✓ |
| SERIALIZABLE | ❌ | ❌ | ❌ |
Transaction Timeout
@Transactional(timeout = 30) // 30 seconds timeout
public void longRunningOperation() {
// If this takes > 30 seconds, transaction rolls back
}
Programmatic Transaction Management
@Autowired
private TransactionTemplate transactionTemplate;
public void someMethod() {
transactionTemplate.execute(status -> {
// Your transactional code here
if (somethingWrong) {
status.setRollbackOnly(); // Manual rollback
}
return result;
});
}
Quick Reference: Common Solutions
Solution 1: Add @Transactional to Service
@Transactional
public Rule create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
return repository.save(rule);
}
Solution 2: Change LAZY to EAGER
@ElementCollection(fetch = FetchType.EAGER) // Load immediately
private Set<ServiceDefinition> serviceDefinitions;
Solution 3: Initialize Lazy Collections
public Rule create(RuleRequestDTO dto) {
Rule rule = RuleConverter.toEntity(dto);
Rule saved = repository.save(rule);
// Force lazy loading while session is open
saved.getServiceDefinitions().size();
return saved;
}
Solution 4: Use Fetch Join
@Query("SELECT r FROM Rule r LEFT JOIN FETCH r.serviceDefinitions WHERE r.id = :id")
Rule findByIdWithServiceDefinitions(@Param("id") Long id);
Summary
Key Takeaways
- @Transactional keeps the database connection open for the duration of the method
- Lazy-loaded data needs an open session to be accessed
- Without @Transactional, entities become detached after repository operations
- Use @Transactional on service layer methods that need database access
- Multiple database operations are atomic within a transaction
The Golden Rule
If your method returns an entity with lazy-loaded relationships, and those relationships will be accessed later, use @Transactional!
When to Use @Transactional
✓ Service methods that perform database operations
✓ Methods that access lazy-loaded collections
✓ Operations that need to be atomic (all-or-nothing)
✓ Methods that modify multiple entities
When NOT to Use @Transactional
❌ Controller methods (keep controllers thin)
❌ Simple read operations that don't access lazy data
❌ Methods that don't interact with database
Further Reading
- Spring Documentation: Transaction Management
- Hibernate User Guide: Persistence Context
- Baeldung: Spring @Transactional
Remember: Think of @Transactional as keeping the phone line to the database open. Without it, you're trying to ask questions after hanging up! 📞
Top comments (0)