What is the Difference Between Lazy and Eager Loading
FetchType determines when Hibernate loads related entities: immediately with the parent (EAGER) or only on access (LAZY). This is a fundamental decision affecting performance an...
Overview
FetchType determines when Hibernate loads related entities: immediately with the parent (EAGER) or only on access (LAZY). This is a fundamental decision affecting performance and application architecture.
Analogy: Imagine loading a book. EAGER - also loads all reviews, comments, and revision history. LAZY - loads only the book, and pulls reviews when the user clicks the tab.
Junior Level
Definitions
LAZY (lazy loading) - related data is loaded only on first access.
EAGER (eager loading) - related data is loaded immediately with the main object.
// EAGER - loads items immediately
@OneToMany(fetch = FetchType.EAGER)
private List<OrderItem> items;
Order order = entityManager.find(Order.class, 1L);
// items is ALREADY loaded!
// LAZY - loads items only on access
@OneToMany(fetch = FetchType.LAZY)
private List<OrderItem> items;
Order order = entityManager.find(Order.class, 1L);
// items is NOT YET loaded - this is a proxy
order.getItems().size(); // ONLY HERE does the DB query happen
Default Values
| Annotation | Default FetchType |
|---|---|
| @ManyToOne | EAGER |
| @OneToMany | LAZY |
| @OneToOne | EAGER |
| @ManyToMany | LAZY |
Default values are for JPA 2.x. In Hibernate 5/6 you can override via @LazyToOne.
When to Use What
- LAZY - almost always, especially for collections
- EAGER - only for small mandatory associations needed in 100% of cases
Middle Level
LazyInitializationException
The most common problem with LAZY:
@Transactional
public Order getOrder(Long id) {
return entityManager.find(Order.class, id);
}
// Outside transaction:
Order order = service.getOrder(1L);
order.getItems().size(); // LazyInitializationException!
// Hibernate session is already closed, proxy cannot load data
Solutions
// 1. JOIN FETCH - load within transaction
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
// 2. Hibernate.initialize() - forced initialization
@Transactional
public Order getOrderWithItems(Long id) {
Order order = entityManager.find(Order.class, id);
Hibernate.initialize(order.getItems());
return order;
}
// 3. @EntityGraph - dynamic loading
@EntityGraph(attributePaths = {"items"})
Order findById(Long id);
// 4. Initialization in service layer
@Transactional(readOnly = true)
public OrderDto getOrderDto(Long id) {
Order order = repository.findById(id).orElseThrow();
order.getItems().size(); // force initialization
return OrderDto.from(order);
}
Open Session In View - Antipattern
// Not recommended in production
spring.jpa.open-in-view: true
This mode keeps the session open until view rendering completes, leading to:
- Long transactions
- Connection leaks
- Hidden performance problems
Common Mistakes
// EAGER for large collections
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
// Loads ALL orders -> OutOfMemoryError
// EAGER "just in case"
@ManyToOne(fetch = FetchType.EAGER)
private User user;
// Even when user is not needed - extra JOIN
Senior Level
Internal Implementation
How LAZY loading works:
1. Hibernate creates a proxy for the related entity/collection
2. For @ManyToOne - ByteBuddy proxy extending the class
ByteBuddy is a library for creating subclasses at runtime. Hibernate uses it to create a subclass of your entity that intercepts field accesses and loads data from the DB on first access.
3. For @OneToMany - PersistentCollection (PersistentBag/PersistentSet)
4. On proxy access:
- Checks Session.isOpen()
- If open -> executes SELECT
- If closed -> LazyInitializationException
5. After loading, proxy is replaced with the real object
Why @ManyToOne is EAGER by default:
@ManyToOne references ONE entity (e.g., Order -> User). Usually you need the user along with the order. @OneToMany references a COLLECTION (Order -> OrderItems), which may contain hundreds of records - loading them all every time would be wasteful.
A historical decision in the JPA specification, based on the assumption that the parent entity is usually needed along with the child. In practice, this is rarely justified.
Architectural Trade-offs
| LAZY | EAGER |
|---|---|
| More flexible and performant | Simpler to develop |
| Requires planning data loading | Always available |
| Can cause LazyInitializationException | Can cause N+1 problem |
| Suitable for high-load systems | Suitable for prototypes |
“LAZY by Default” Strategy
RULE: Declare all associations as LAZY, load explicitly when needed.
Why this is correct:
1. EAGER cannot be cancelled in a specific query
2. LAZY can be "turned into" EAGER via JOIN FETCH
3. LAZY provides maximum flexibility
@Entity
public class Order {
// Override default EAGER to LAZY
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// LAZY - default for collections
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
DTO Projection Instead of EAGER
// Instead of loading full entities with EAGER associations
@Query("""
SELECT new com.example.OrderSummaryDto(
o.id, o.date, o.status, u.name, COUNT(i)
)
FROM Order o
JOIN o.user u
LEFT JOIN o.items i
WHERE o.id = :id
GROUP BY o.id, u.name
""")
OrderSummaryDto findOrderSummary(@Param("id") Long id);
Best Practices
LAZY by default for the vast majority of associations. EAGER only when you've measured and confirmed the association is needed in 100% of requests.
Override @ManyToOne and @OneToOne to LAZY
JOIN FETCH when related data is needed
@EntityGraph for dynamic loading
DTO projection for API responses
Initialize within @Transactional methods
EAGER for collections (@OneToMany, @ManyToMany)
Open Session In View in production
Accessing LAZY fields outside transaction
EAGER "just in case"
Interview Cheat Sheet
Must know:
- LAZY - data loads on first access, EAGER - loads immediately
- Defaults: @ManyToOne = EAGER, @OneToMany = LAZY, @OneToOne = EAGER, @ManyToMany = LAZY
- LAZY by default is the best strategy for all associations
- EAGER cannot be cancelled in a query, LAZY can be “turned into” EAGER via JOIN FETCH
- LAZY works via proxy (ByteBuddy) and PersistentCollection
- LazyInitializationException occurs when accessing LAZY outside a transaction
- DTO projection is the best alternative to EAGER for APIs
Frequent follow-up questions:
- Why is LAZY better than EAGER? LAZY is flexible - can load when needed; EAGER is intrusive - cannot be cancelled
- Why is @ManyToOne EAGER by default? Historical JPA decision - assumed parent entity is usually needed
- How to solve LazyInitializationException? JOIN FETCH, EntityGraph, initialization inside @Transactional, DTO projection
- What is Open Session In View? Session stays open until view rendering - antipattern for production
Red flags (DO NOT say):
- “EAGER is simpler - I use it” - creates N+1 and memory problems
- “I solve LazyInitializationException via EAGER” - treats the symptom, creates a new problem
- “LAZY only for large collections” - LAZY for ALL associations
- “Open Session In View is normal practice” - antipattern for production
Related topics:
- [[3. When to Use Lazy vs Eager Loading]]
- [[4. What is LazyInitializationException and How to Avoid It]]
- [[1. What is the N+1 Problem and How to Solve It]]
- [[5. What Fetch Strategies Exist in Hibernate]]
- [[29. What is Projection in JPA]]