Question 4 · Section 16

What is LazyInitializationException and How to Avoid It

LazyInitializationException is one of the most common exceptions in Hibernate. It occurs when attempting to access a lazily loaded association after the Hibernate session has be...

Language versions: English Russian Ukrainian

Overview

LazyInitializationException is one of the most common exceptions in Hibernate. It occurs when attempting to access a lazily loaded association after the Hibernate session has been closed. Understanding the causes and prevention methods is a key skill for working with JPA/Hibernate.


Junior Level

What is LazyInitializationException

This exception occurs when you try to access a lazily loaded field (LAZY) and the Hibernate session is already closed.

Session (EntityManager) is a connection to the DB. In Spring, it opens when entering an @Transactional method and closes when exiting. Outside a transaction, Hibernate cannot execute an SQL query.

@Transactional
public Order getOrder(Long id) {
    return entityManager.find(Order.class, id);
    // Session is open, but after exiting the method - it closes
}

// Outside transaction:
Order order = service.getOrder(1L);
order.getItems().size();  // LazyInitializationException!
// Session is closed, Hibernate cannot load items

Why It Occurs

1. Order.getItems() is a proxy (PersistentCollection)
2. On access, Hibernate tries to load data from the DB
3. But the session (EntityManager) is already closed
4. Hibernate cannot execute SELECT -> exception

How to Avoid - Basic Approaches

  1. JOIN FETCH - load data before session closes
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Order findByIdWithItems(@Param("id") Long id);
    
  2. @EntityGraph - specify what to load
    @EntityGraph(attributePaths = {"items"})
    Order findById(Long id);
    
  3. Hibernate.initialize() - force initialization
    @Transactional
    public Order getOrderWithItems(Long id) {
     Order order = entityManager.find(Order.class, id);
     Hibernate.initialize(order.getItems());  // load items
     return order;
    }
    

How to Choose the Right Solution

  • JOIN FETCH - when you know exactly what’s needed in THIS query
  • EntityGraph - when you need to dynamically choose what to load
  • Hibernate.initialize() - when you need to initialize in the service layer without writing JPQL

Middle Level

Detailed Solutions

1. JOIN FETCH in Repository

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    @Query("SELECT DISTINCT o FROM Order o " +
           "JOIN FETCH o.user u " +
           "JOIN FETCH o.items i " +
           "WHERE o.id = :id")
    Optional<Order> findByIdWithUserAndItems(@Param("id") Long id);
}

2. Initialization in Service Layer

@Service
@RequiredArgsConstructor
public class OrderService {
    private final EntityManager entityManager;

    @Transactional(readOnly = true)
    public OrderDto getOrderDto(Long id) {
        Order order = entityManager.find(Order.class, id);

        // Initialize everything needed here, while session is open
        order.getItems().size();       // force load items
        order.getUser().getName();     // force load user
        order.getAddress().getCity();  // force load address

        return OrderDto.from(order);
    }
}

3. @EntityGraph for Dynamic Loading

public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = {"user", "items"})
    @Query("SELECT o FROM Order o WHERE o.id = :id")
    Optional<Order> findWithUserAndItems(@Param("id") Long id);
}
// Not recommended in production, but some teams deliberately enable OSIV in Spring Boot to simplify development, accepting the trade-off. This is acceptable if the team monitors SQL queries and controls N+1.
spring.jpa.open-in-view: true

OSIV (Open Session In View) is a pattern where the Hibernate session lives throughout the entire HTTP request, including the rendering layer. “View” in MVC refers to JSP/Thymeleaf. In REST applications, JSON serialization plays the “view” role.

OSIV Problems:

  • Session stays open until view rendering (including JSON serialization)
  • Long transactions -> holding DB connections
  • Hidden performance problems (N+1 not visible)
  • Potential connection leaks under high load

Common Mistakes

// "Fixing" via EAGER
@OneToMany(fetch = FetchType.EAGER)
private List<OrderItem> items;
// "Fixed" LazyInitializationException but created N+1 problem

// Accessing lazy fields in controller
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        Order order = service.getOrder(id);  // session already closed
        return order;  // JSON serialization -> LazyInitializationException
    }
}

Senior Level

Internal Implementation

Exception mechanism:

1. Order.items -> PersistentCollection proxy
2. On access order.getItems():
   - Hibernate.checkTransactionState()
   - session.isOpen() ? yes -> load from DB
   - session.isOpen() ? no -> throw LazyInitializationException

3. PersistentCollection holds a reference to Session
4. When Session is closed -> reference becomes invalid
5. Proxy cannot load data -> exception

Architectural Approaches

1. DTO at Transaction Boundary

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository repository;

    @Transactional(readOnly = true)
    public OrderDto getOrderDto(Long id) {
        // Inside @Transactional - session is open
        Order order = repository.findById(id).orElseThrow();

        // Map to DTO while session is open
        return OrderDto.from(order);
    }
    // Session closes - but DTO no longer depends on Hibernate
}

2. Specification Pattern for Dynamic Fetch Plans

public class OrderSpecifications {

    public static Specification<Order> withUser() {
        return (root, query, cb) -> {
            // Specification returns null in fetch scenarios because we're not filtering
            // data, only modifying the fetch plan. No Predicate needed.
            root.fetch("user", JoinType.LEFT);
            return null;
        };
    }

    public static Specification<Order> withItems() {
        return (root, query, cb) -> {
            // Specification returns null in fetch scenarios because we're not filtering
            // data, only modifying the fetch plan. No Predicate needed.
            root.fetch("items", JoinType.LEFT);
            return null;
        };
    }
}

// Usage:
List<Order> orders = repository.findAll(
    Specification.where(OrderSpecifications.withUser())
        .and(OrderSpecifications.withItems())
);

Production Experience

// For REST API - always return DTOs
@RestController
@RequiredArgsConstructor
public class OrderController {
    private final OrderService service;

    @GetMapping("/orders/{id}")
    public OrderDto getOrder(@PathVariable Long id) {
        return service.getOrderDto(id);  // DTO, no lazy issues
    }
}

// For batch processing - initialization in loop
@Transactional
public void processAllOrders() {
    List<Order> orders = repository.findAll();
    for (Order order : orders) {
        // Initialization within transaction
        order.getItems().forEach(Item::calculateTotal);
        order.getUser().getDiscount();
        // Processing...
    }
}

Best Practices

JOIN FETCH in queries when data is needed
Initialization inside @Transactional methods
@EntityGraph for dynamic loading
DTO projection for API responses
Mapping to DTO at transaction boundary
Hibernate.initialize() for rare cases

Open Session In View in production
Accessing lazy fields outside transaction
"Fixing" via EAGER "to avoid"
Ignoring LazyInitializationException
Serializing entities directly to JSON

Interview Cheat Sheet

Must know:

  • LazyInitializationException occurs when accessing a LAZY field after session closes
  • Session (EntityManager) opens on entering @Transactional and closes on exiting
  • 3 main solutions: JOIN FETCH, EntityGraph, Hibernate.initialize()
  • DTO projection is the best approach: mapping at transaction boundary
  • Open Session In View is an antipattern for production (long transactions, hidden problems)
  • REST controllers should return DTOs, not entities

Frequent follow-up questions:

  • How to choose the solution? JOIN FETCH - when you know exactly what’s needed; EntityGraph - dynamically; Hibernate.initialize() - in service layer
  • Why is OSIV not recommended? Long transactions, connection leaks, hidden N+1
  • Can it be solved via EAGER? Technically yes, but it creates the N+1 problem - incorrect solution

Red flags (DO NOT say):

  • “I enable EAGER to avoid LazyInitializationException” - creates N+1
  • “Open Session In View is good practice” - antipattern for production
  • “I serialize entities directly to JSON” - StackOverflowError + LazyInitializationException
  • “I fix it via spring.jpa.open-in-view=true” - masks the problem

Related topics:

  • [[2. What is the Difference Between Lazy and Eager Loading]]
  • [[3. When to Use Lazy vs Eager Loading]]
  • [[28. How to Use JOIN FETCH to Solve the N+1 Problem]]
  • [[25. How to Avoid Infinite Recursion When Serializing Entities]]
  • [[29. What is Projection in JPA]]