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...
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
- 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); - @EntityGraph - specify what to load
@EntityGraph(attributePaths = {"items"}) Order findById(Long id); - 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);
}
Open Session In View - Why Not Recommended
// 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]]