Full Stack • Java • System Design • Cloud • AI Engineering

Lazy Loading vs Eager Loading in Hibernate and JPA

Learn Lazy Loading and Eager Loading in Hibernate and JPA with FetchType.LAZY, FetchType.EAGER, SQL queries, N+1 problem, code examples, performance impact, best practices, and interview questions.

What You Will Learn

  • What is Fetching?
  • What is Lazy Loading?
  • What is Eager Loading?
  • FetchType.LAZY
  • FetchType.EAGER
  • OneToMany Example
  • ManyToOne Example
  • SQL Query Comparison
  • LazyInitializationException
  • N Plus One Query Problem
  • Best Practices
  • Interview Questions

Introduction

Hibernate loads data from the database using entity relationships.

Example:

Customer

has many

Orders

When we load a customer, should Hibernate also load all orders immediately?

There are two options:

Lazy Loading

Eager Loading

Understanding this is very important for performance.


What is Fetching?

Fetching means:

How Hibernate loads related entities from the database

Example:

Customer customer = repository.findById(1L).get();

Question:

Should customer orders also be loaded now?

That depends on fetch type.


Entity Relationship Example

flowchart LR
    Customer --> Orders

A customer can have many orders.


Lazy Loading

Lazy Loading means:

Load related data only when it is needed

Hibernate first loads only the main entity.

Related entities are loaded later when accessed.


Lazy Loading Flow

flowchart LR
    A[Load Customer]
    B[Customer Loaded]
    C[Access Orders]
    D[Orders Loaded]

    A --> B
    B --> C
    C --> D

Eager Loading

Eager Loading means:

Load related data immediately

When main entity is loaded, related entities are also loaded.


Eager Loading Flow

flowchart LR
    A[Load Customer]
    B[Load Orders]
    C[Return Full Object]

    A --> B
    B --> C

Database Tables

Customer Table

CREATE TABLE customers (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

Order Table

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_number VARCHAR(100),
    amount DECIMAL(10,2),
    customer_id BIGINT
);

Customer Entity

import jakarta.persistence.*;
import java.util.List;

@Entity
@Table(name = "customers")
public class Customer {

    @Id
    private Long id;

    private String name;

    private String email;

    @OneToMany(
        mappedBy = "customer",
        fetch = FetchType.LAZY
    )
    private List<Order> orders;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List<Order> getOrders() {
        return orders;
    }
}

Order Entity

import jakarta.persistence.*;

@Entity
@Table(name = "orders")
public class Order {

    @Id
    private Long id;

    private String orderNumber;

    private Double amount;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    public Long getId() {
        return id;
    }

    public String getOrderNumber() {
        return orderNumber;
    }

    public Customer getCustomer() {
        return customer;
    }
}

Lazy Loading Example

Customer customer =
        customerRepository.findById(1L)
                .orElseThrow();

System.out.println(
        customer.getName()
);

Generated SQL:

SELECT *
FROM customers
WHERE id = 1;

Orders are not loaded yet.


Accessing Lazy Collection

Customer customer =
        customerRepository.findById(1L)
                .orElseThrow();

List<Order> orders =
        customer.getOrders();

System.out.println(
        orders.size()
);

Now Hibernate loads orders.

Generated SQL:

SELECT *
FROM orders
WHERE customer_id = 1;

Lazy Loading Internal Flow

flowchart LR
    A[Repository Call]
    B[Load Customer]
    C[Create Proxy]
    D[Access Orders]
    E[Load Orders]

    A --> B
    B --> C
    C --> D
    D --> E

What is Proxy?

For Lazy Loading, Hibernate creates a proxy object.

Proxy means:

Temporary placeholder object

It loads real data only when accessed.


Proxy Example

customer.orders

is not real list initially

it is Hibernate proxy

When we call:

customer.getOrders().size();

Hibernate executes SQL.


Eager Loading Example

Change relationship:

@OneToMany(
    mappedBy = "customer",
    fetch = FetchType.EAGER
)
private List<Order> orders;

Now load customer:

Customer customer =
        customerRepository.findById(1L)
                .orElseThrow();

Hibernate loads customer and orders immediately.


Possible Generated SQL

SELECT c.*, o.*
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
WHERE c.id = 1;

or Hibernate may execute separate select depending on mapping and provider behavior.


Lazy vs Eager Query Flow

flowchart LR
    A[Lazy Loading]
    B[Load Main Entity Only]
    C[Eager Loading]
    D[Load Main Entity And Relations]

    A --> B
    C --> D

Default Fetch Types

Very important interview point.

Relationship Default Fetch
OneToOne EAGER
ManyToOne EAGER
OneToMany LAZY
ManyToMany LAZY

Recommended Practice

Even though ManyToOne default is EAGER, many teams prefer:

@ManyToOne(fetch = FetchType.LAZY)

Why?

Avoid unnecessary joins

Improve performance

Prevent large object graphs

ManyToOne Lazy Example

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;

Load order:

Order order =
        orderRepository.findById(101L)
                .orElseThrow();

Generated SQL:

SELECT *
FROM orders
WHERE id = 101;

Customer loads only when accessed:

order.getCustomer().getName();

Then SQL:

SELECT *
FROM customers
WHERE id = 1;

LazyInitializationException

Common production error.

It happens when:

Lazy data is accessed after Hibernate Session is closed

Problem Example

public Customer getCustomer() {

    Customer customer =
            customerRepository.findById(1L)
                    .orElseThrow();

    return customer;
}

Later outside transaction:

customer.getOrders().size();

Error:

LazyInitializationException

Why It Happens

flowchart LR
    A[Load Customer]
    B[Session Closed]
    C[Access Orders]
    D[Exception]

    A --> B
    B --> C
    C --> D

Hibernate cannot load orders because session is closed.


Solution 1: Use Transactional

@Transactional
public CustomerDTO getCustomerWithOrders(Long id) {

    Customer customer =
            customerRepository.findById(id)
                    .orElseThrow();

    int orderCount =
            customer.getOrders().size();

    return mapper.toDTO(customer);
}

Transaction keeps session open.


Solution 2: Fetch Join

@Query("""
    SELECT c
    FROM Customer c
    LEFT JOIN FETCH c.orders
    WHERE c.id = :id
""")
Customer findCustomerWithOrders(
        Long id
);

This loads customer and orders in one query.


Generated SQL

SELECT c.*, o.*
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
WHERE c.id = 1;

Solution 3: EntityGraph

@EntityGraph(attributePaths = "orders")
Optional<Customer> findById(
        Long id
);

EntityGraph tells JPA to load orders for this query.


Solution 4: DTO Projection

public record CustomerOrderDTO(
        Long customerId,
        String customerName,
        Long orderId,
        Double amount
) {
}

Query:

@Query("""
    SELECT new com.codewithvenu.dto.CustomerOrderDTO(
        c.id,
        c.name,
        o.id,
        o.amount
    )
    FROM Customer c
    JOIN c.orders o
    WHERE c.id = :id
""")
List<CustomerOrderDTO> findCustomerOrders(
        Long id
);

N Plus One Query Problem

Lazy loading can cause N Plus One problem.

Example:

List<Customer> customers =
        customerRepository.findAll();

for (Customer customer : customers) {
    System.out.println(
            customer.getOrders().size()
    );
}

Generated queries:

1 query to load customers

N queries to load orders for each customer

N Plus One Flow

flowchart LR
    A[Load Customers]
    B[Load Orders For Customer 1]
    C[Load Orders For Customer 2]
    D[Load Orders For Customer 3]

    A --> B
    A --> C
    A --> D

N Plus One SQL Example

SELECT *
FROM customers;

Then:

SELECT *
FROM orders
WHERE customer_id = 1;

SELECT *
FROM orders
WHERE customer_id = 2;

SELECT *
FROM orders
WHERE customer_id = 3;

Fix N Plus One With Fetch Join

@Query("""
    SELECT DISTINCT c
    FROM Customer c
    LEFT JOIN FETCH c.orders
""")
List<Customer> findAllWithOrders();

Fix N Plus One With EntityGraph

@EntityGraph(attributePaths = "orders")
@Query("SELECT c FROM Customer c")
List<Customer> findAllWithOrders();

Fix N Plus One With Batch Size

@BatchSize(size = 10)
@OneToMany(
    mappedBy = "customer",
    fetch = FetchType.LAZY
)
private List<Order> orders;

Hibernate loads orders in batches.


Batch Size Concept

Without batch:

Customer 1 orders

Customer 2 orders

Customer 3 orders

With batch:

Orders for customers 1 to 10 loaded together

Lazy Loading Advantages

✅ Better performance when relations are not needed

✅ Lower memory usage

✅ Avoids unnecessary joins

✅ Better for large object graphs


Lazy Loading Disadvantages

❌ LazyInitializationException

❌ N Plus One problem

❌ Requires transaction awareness

❌ Hidden SQL execution


Eager Loading Advantages

✅ Related data immediately available

✅ Avoids LazyInitializationException

✅ Useful for small mandatory relationships


Eager Loading Disadvantages

❌ Loads unnecessary data

❌ Large joins

❌ Slower queries

❌ More memory usage

❌ Can cause performance problems


Lazy vs Eager Comparison

Feature Lazy Loading Eager Loading
Load Time On Demand Immediately
Performance Better Usually Can Be Slower
Memory Usage Lower Higher
Risk LazyInitializationException Over Fetching
Best For Large Relations Small Required Data

Banking Example

Account has many transactions.

@OneToMany(
    mappedBy = "account",
    fetch = FetchType.LAZY
)
private List<Transaction> transactions;

Why lazy?

Account may have thousands of transactions

Do not load all transactions when viewing account summary.


Insurance Example

Policy has many claims.

@OneToMany(
    mappedBy = "policy",
    fetch = FetchType.LAZY
)
private List<Claim> claims;

Load claims only when user opens claim history.


E-Commerce Example

Product has category.

@ManyToOne(fetch = FetchType.LAZY)
private Category category;

Load category only when required.


Real Enterprise Rule

Use:

LAZY by default

Then load required data using:

Fetch Join

EntityGraph

DTO Projection

Production Best Practices

✅ Prefer LAZY for relationships

✅ Avoid global EAGER mappings

✅ Use fetch join for specific use cases

✅ Use DTO projections for API responses

✅ Monitor generated SQL

✅ Fix N Plus One early

✅ Never expose entities directly from REST APIs


Debug SQL Logging

Add this to application.yml:

spring:
  jpa:
    show-sql: true

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE

This helps you see SQL generated by Hibernate.


Interview Questions

What is Lazy Loading?

Lazy Loading loads related entities only when accessed.


What is Eager Loading?

Eager Loading loads related entities immediately with the main entity.


Default Fetch Type for OneToMany?

LAZY

Default Fetch Type for ManyToOne?

EAGER

What is LazyInitializationException?

An exception thrown when lazy data is accessed after Hibernate session is closed.


How to Fix LazyInitializationException?

Transactional method

Fetch Join

EntityGraph

DTO Projection

What is N Plus One Problem?

One query loads parent records, then N additional queries load child records.


Best Practice?

Use LAZY by default and fetch required relationships explicitly.


Key Takeaways

  • Lazy Loading loads relationships only when accessed.
  • Eager Loading loads relationships immediately.
  • Lazy Loading is usually better for performance.
  • Eager Loading can cause unnecessary data loading.
  • LazyInitializationException happens when session is closed.
  • N Plus One problem is common with Lazy Loading.
  • Use Fetch Join, EntityGraph, DTO Projections, and BatchSize to control performance.
  • In enterprise applications, prefer LAZY by default and fetch only what each use case needs.