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.