DEV Community

Cover image for Understanding @Transactional in Spring Boot
Md. Monowarul Amin
Md. Monowarul Amin

Posted on • Edited on

Understanding @Transactional in Spring Boot

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
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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              │
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

❌ Without @Transactional:

public Order create(OrderDTO dto) {
    Order order = new Order();
    return repository.save(order);
    // Session closes here!
}
// Controller accesses order.getItems() → 💥 LazyInitializationException
Enter fullscreen mode Exit fullscreen mode

✅ With @Transactional:

@Transactional
public Order create(OrderDTO dto) {
    Order order = new Order();
    return repository.save(order);
    // Session stays open — lazy collections accessible
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 ✓
}
Enter fullscreen mode Exit fullscreen mode

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 ❌)
Enter fullscreen mode Exit fullscreen mode

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 ❌
}
Enter fullscreen mode Exit fullscreen mode

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
    └─────────────────────────────┘
}
Enter fullscreen mode Exit fullscreen mode

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() { ... }
}
Enter fullscreen mode Exit fullscreen mode

2. Use readOnly = true for Query Methods

@Transactional(readOnly = true)
public List<Order> findAll() {
    return repository.findAll();
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

4. Specify Rollback Rules When Needed

@Transactional(rollbackFor = Exception.class)
public void processPayment() { ... }

@Transactional(noRollbackFor = AuditException.class)
public void logActivity() { ... }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Option 2: Force initialization while session is open

saved.getItems().size(); // triggers lazy load
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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! 📞


Further Reading

Top comments (0)