Question 3 · Section 16

When to Use Lazy vs Eager Loading

The choice between LAZY and EAGER is a choice between scalability and development convenience. In modern Java/Spring development, there's a "golden rule": use LAZY by default fo...

Language versions: English Russian Ukrainian

Overview

The choice between LAZY and EAGER is a choice between scalability and development convenience. In modern Java/Spring development, there’s a “golden rule”: use LAZY by default for the vast majority of associations.


Junior Level

Basic Rule

Almost always use LAZY. EAGER - only when you know exactly why.

LAZY when:

  • @OneToMany or @ManyToMany association
  • Related data is not always needed
  • Large collections
  • Data may be heavy to load

EAGER when:

  • @ManyToOne and data is always needed
  • Small, mandatory associations (reference data)
  • Data is needed in 100% of parent entity usages
// LAZY - items not always needed
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;

// LAZY - user not always needed
@ManyToOne(fetch = FetchType.LAZY)
private User user;

// EAGER makes sense only for small mandatory associations
// UserProfile is a small entity, always displayed with User,
// without it User is incomplete. EAGER is justified here.
@OneToOne(fetch = FetchType.EAGER)
private UserProfile profile;  // always needed with User

When LAZY is a Bad Choice

  1. Batch processing when all related data is needed anyway - better to load immediately via JOIN FETCH
  2. Small reference tables (1-10 records) - lazy loading overhead exceeds the benefit
  3. Entities that are ALWAYS needed together - two separate queries are slower than one JOIN

Middle Level

Practical Recommendations

@ManyToOne -> LAZY (almost always, despite the EAGER default)
@OneToMany -> LAZY (default, don't change)
@OneToOne -> LAZY (often forgotten to override)
@ManyToMany -> LAZY (default, don't change)

Why Override @ManyToOne to LAZY

@Entity
public class Order {
    // Default is EAGER, but we override
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}

// When loading Order without JOIN:
// Without LAZY: SELECT o.*, u.* FROM orders o JOIN users u ON ...
// With LAZY: SELECT o.* FROM orders o (user loads only on access)

Examples of Correct Decisions

// LAZY for collection
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;

// LAZY for @ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

// LAZY for @OneToOne
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "address_id")
private Address address;

// Load when needed - via JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o " +
       "JOIN FETCH o.user " +
       "JOIN FETCH o.items " +
       "WHERE o.id = :id")
Order findByIdComplete(@Param("id") Long id);

Common Mistakes

// EAGER for large collections
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
// Loads ALL orders -> performance degradation

// EAGER "just in case"
@ManyToOne(fetch = FetchType.EAGER)
private Category category;
// Even when category is not displayed - extra JOIN

Senior Level

Architectural Trade-offs

LAZY EAGER
More flexible and scalable Simpler to develop
Need to think about loading Always available
Can cause LazyInitializationException Can cause N+1 problem
Dynamic loading Static loading
Suitable for microservices Suitable for monoliths

Senior Developer Strategy: “LAZY + Dynamic Loading”

Instead of statically declaring FetchType in the Entity, use dynamic loading:

1. Globally: LAZY for all associations
2. Locally: FETCH when needed in a specific query
   - JPQL: JOIN FETCH
   - Spring Data JPA: @EntityGraph
   - Criteria API: root.fetch("association")

Why “LAZY by Default” is Correct

If you declare an association as EAGER:
-> You can NEVER make it lazy in a specific query
-> Hibernate will still fetch the data

If you declare an association as LAZY:
-> You can "turn it into" EAGER at any time via JOIN FETCH
-> LAZY provides maximum flexibility

DTO Projection Instead of EAGER

// Instead of EAGER loading - use DTOs for APIs
@Query("""
    SELECT new com.example.OrderDto(
        o.id, o.date, o.status, u.name, u.email
    )
    FROM Order o JOIN o.user u
    WHERE o.id = :id
    """)
OrderDto findOrderDto(@Param("id") Long id);

// Advantages:
// 1. One query instead of two
// 2. Less data in memory
// 3. No serialization problems
// 4. Controlled API contract

Microservice Architecture

In microservices with a shared DB (not recommended, but it happens) LAZY is especially important because extra data = extra load on a shared resource. In proper microservices with separate DBs, this question is resolved at the API contract level.

Production Experience

// Spring Data JPA with @EntityGraph
public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = {"user", "items"})
    Optional<Order> findByIdWithDetails(Long id);

    @EntityGraph(attributePaths = {"user"})
    List<Order> findByUserId(Long userId);

    // Without EntityGraph - only Order without associations
    List<Order> findByStatus(String status);
}

Best Practices

LAZY by default for ALL associations
Override @ManyToOne and @OneToOne to LAZY
JOIN FETCH when data is needed
@EntityGraph for dynamic loading
DTO projection for read-only/API
Monitor DB queries

EAGER for collections
EAGER "just in case"
Solving LazyInitializationException via EAGER
Loading entities for simple read-only operations

Summary for Senior

  • EAGER - static and intrusive, cannot be cancelled
  • LAZY - dynamic and flexible, can load when needed
  • Override default EAGER associations (OneToOne, ManyToOne) to LAZY
  • Solve LazyInitializationException through proper transaction design or Entity Graphs, not by switching to EAGER

Interview Cheat Sheet

Must know:

  • Golden rule: LAZY by default for ALL associations
  • Override @ManyToOne and @OneToOne to LAZY (their default is EAGER)
  • EAGER cannot be cancelled in a query, LAZY can be loaded via JOIN FETCH
  • LAZY provides maximum flexibility - dynamic loading when needed
  • DTO projection for APIs instead of loading full entities
  • In microservices, LAZY is especially important to reduce DB load

Frequent follow-up questions:

  • When is EAGER justified? Small mandatory associations (reference data), data needed in 100% of cases
  • Why override @ManyToOne to LAZY? Even when user is not needed - extra JOIN without LAZY
  • What to do about LazyInitializationException? Proper transaction design, Entity Graphs, DTO - NOT switching to EAGER

Red flags (DO NOT say):

  • “EAGER for @ManyToOne - it’s the default, I don’t touch it” - needs overriding to LAZY
  • “I solve LazyInitializationException via EAGER” - creates N+1
  • “LAZY only for collections” - LAZY for all associations including @ManyToOne

Related topics:

  • [[2. What is the Difference Between Lazy and Eager Loading]]
  • [[4. What is LazyInitializationException and How to Avoid It]]
  • [[5. What Fetch Strategies Exist in Hibernate]]
  • [[29. What is Projection in JPA]]