Question 2 · Section 16

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...

Language versions: English Russian Ukrainian

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]]