Part 7 — Lazy Loading vs Eager Loading in Hibernate (And Why LazyInitializationException Happens)
Introduction
Part 7 — Lazy Loading vs Eager Loading in Hibernate (And Why LazyInitializationException Happens)
Introduction
In the previous articles, we explored the Persistence Context, Dirty Checking, and Flush vs Commit. Now, an important question arises:
👉 How does Hibernate fetch related data?
When an entity has relationships — like an Employee that has many Addresses—should Hibernate:
- load everything immediately?
- or load only when needed?
This choice is controlled by two fundamental strategies:
- Lazy Loading — load on demand
- Eager Loading — load immediately

In this article, we will uncover:
- What Lazy Loading and Eager Loading are
- How they work internally
- The dreaded
LazyInitializationException - When to use each strategy
- Common interview traps and best practices
Let’s dive in! 🚀
What is Fetching in Hibernate?
Fetching defines how and when related entities are loaded from the database. It’s configured using the fetch attribute of JPA annotations like @OneToMany, @ManyToOne, etc.
@Entity
public class Employee {
@OneToMany(fetch = FetchType.LAZY)
private List<Address> addresses;
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
}
Here, Hibernate must decide:
- Should the
addresseslist be loaded right away or only when we callgetAddresses()? - Should the
departmentbe loaded together with the employee, or later?
These decisions have a huge impact on performance and the correctness of your application.
Lazy Loading
Lazy loading means that the related data is not loaded immediately when the parent entity is loaded. Instead, it is fetched only when you first access it.
@Entity
public class Employee {
@OneToMany(fetch = FetchType.LAZY)
private List<Address> addresses;
}
How It Works
- You load an
Employeefrom the database. - The
addresseslist is not loaded—it’s replaced by a Hibernate proxy (an empty placeholder). - When you call
employee.getAddresses()for the first time, Hibernate detects that the proxy hasn’t been initialized, so it executes a query to fetch the addresses. - Subsequent calls use the already loaded data.
Visual Diagram

Eager Loading
Eager loading means that the related data is loaded immediately together with the parent entity, usually via a SQL JOIN or a separate query.
@Entity
public class Employee {
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
}
How It Works
- When you load an
Employee, Hibernate generates a query that also fetches itsDepartment(either with aJOINor a secondSELECT). - The department data is available right away — no additional query is needed when you call
getDepartment().
Visual Diagram

LazyInitializationException — The Most Feared Exception
This is one of the most common exceptions in Hibernate and a favourite interview question.
What Is It?
LazyInitializationException occurs when you try to access a lazy‑loaded relationship outside of an open Hibernate session (persistence context). Because the session is closed, Hibernate can no longer fetch the data from the database.
Example
public Employee getEmployee(Long id) {
return employeeRepository.findById(id).orElseThrow();
} // transaction ends, session closed
// elsewhere…
Employee emp = getEmployee(1L);
emp.getAddresses(); // ❌ LazyInitializationException!
Why It Happens
- The
getEmployee()method ran without@Transactional(or the transaction ended at method exit). - The
Employeeentity is now detached—no longer associated with a persistence context. - When you call
getAddresses(), Hibernate tries to load the lazy collection, but the session is gone → exception!
Visual Diagram

How to Fix LazyInitializationException
There are several ways to avoid this exception, each with its own trade‑offs.
✅ Solution 1 — Keep the Transaction Open
Annotate the service method with @Transactional so that the session stays alive until you finish using the entity.
@Transactional
public Employee getEmployee(Long id) {
return employeeRepository.findById(id).orElseThrow();
}
Now you can access lazy associations inside the same transactional method (or its callers, as long as the transaction hasn’t ended)
✅ Solution 2 — Use JOIN FETCH in JPQL
If you know you’ll need the related data, fetch it eagerly in the query using JOIN FETCH.
@Query("SELECT e FROM Employee e JOIN FETCH e.addresses")
List<Employee> findAllWithAddresses();j
This loads everything in one query and the data is already in memory — no lazy loading needed.
✅ Solution 3 — Use DTO Projections
Instead of returning entities, project only the fields you need into a DTO. This avoids loading entities altogether and is the most performant option for read‑only use cases.
public interface EmployeeDto {
String getName();
List<AddressDto> getAddresses();
}
✅ Solution 4 — Use Entity Graphs
Define an entity graph to dynamically specify which associations to fetch eagerly for a particular query.
@EntityGraph(attributePaths = {"addresses"})
@Query("SELECT e FROM Employee e WHERE e.id = :id")
Employee findByIdWithAddresses(@Param("id") Long id);
✅ Solution 5— Configure spring.jpa.open-in-view (Not Recommended)
Spring Boot has a property spring.jpa.open-in-view (true by default) that keeps the session open for the whole HTTP request. This can hide the exception but often leads to performance problems and is not a good practice for production.
🔍 Deep Dive: JOIN FETCH vs Regular JOIN
A common point of confusion is the difference between JOIN FETCH and a regular JOIN in JPQL. Both can be used to combine tables, but they behave very differently when it comes to what data is loaded into memory.
Query 1: With JOIN FETCH
@Query("SELECT e FROM Employee e JOIN FETCH e.department WHERE e.id = :id")
Employee findByIdWithDepartment(@Param("id") Long id);
Query 2: With Regular INNER JOIN
@Query("SELECT e FROM Employee e INNER JOIN e.department WHERE e.id = :id")
Employee findByIdWithDepartment(@Param("id") Long id);
Key Differences

What Actually Gets Loaded?
With JOIN FETCH:
- Hibernate executes a SQL query that selects both
employee.*anddepartment.*. - The result set contains all columns for both tables.
- Hibernate constructs the
Employeeentity and also initializes itsdepartmentproperty with the correspondingDepartmententity from the same row. - After the query,
employee.getDepartment()returns the fully loadedDepartmentobject – no lazy loading needed.
With regular INNER JOIN:
- Hibernate executes a SQL join, but selects only columns from the
employeetable. - The result set contains one row per matching employee, but department columns are not retrieved.
- Hibernate constructs the
Employeeentity, but leaves itsdepartmentproperty as a lazy proxy. - If you later call
employee.getDepartment(), Hibernate will execute another SQL query to fetch the department (assuming a session is still open).
Example to Illustrate
Assume we have an Employee with id = 5 belonging to department 'IT'.
JOIN FETCH SQL:
SELECT e.*, d.*
FROM employee e
INNER JOIN department d ON e.department_id = d.id
WHERE e.id = 5;
Result row: (5, 'John', 1, 1, 'IT', 'New York') – Hibernate creates both entities and links them.
Regular INNER JOIN SQL:
SELECT e.*
FROM employee e
INNER JOIN department d ON e.department_id = d.id
WHERE e.id = 5;
Result row: (5, 'John', 1) – department columns are not selected. Hibernate creates only the Employee object; department is a proxy.
When to Use Which

This distinction is crucial for writing efficient Hibernate queries and avoiding the N+1 problem, which we’ll cover in the next article.
Lazy vs Eager — Key Differences

When to Use Lazy vs Eager
Use Lazy Loading When:
- The relationship is a large collection (e.g., an
Orderwith manyOrderItems). - The relationship is optional (you may not always need it).
- You want to avoid loading unnecessary data to improve performance.
Use Eager Loading When:
- The relationship is small (e.g., an
Employeehas oneDepartment). - The data is always needed whenever the parent is loaded.
- You want to avoid the overhead of additional queries (but beware of cartesian products).
Common Interview Traps
Trap 1: Does lazy loading work without an active transaction?
Answer: ❌ No. Lazy loading requires an open Hibernate session (persistence context). Without a transaction, the session is closed, and accessing a lazy property throws LazyInitializationException.
**Trap 2: **Does eager loading always improve performance?
Answer: ❌ No. Eager loading can cause huge JOIN queries that fetch more data than needed, or even multiple SELECTs. It can also lead to cartesian products when multiple collections are fetched eagerly.
Trap 3: How does lazy loading work internally?
Answer: Lazy loading uses proxy objects or collection wrappers. When you access the property, the proxy checks if the data is loaded; if not, it triggers a database query via the still‑open session.
Trap 4: Can we use JOIN FETCH on multiple collections?
Answer: Yes, but be careful — fetching two collections in one query can create a cartesian product, resulting in a huge result set. It’s often better to fetch them separately.
Real‑World Understanding
Think of lazy loading like a smart librarian:
- You ask for a book (the parent entity). The librarian gives you the book but doesn’t bring all the referenced books (related entities) at the same time.
- When you actually need a referenced book, you go back to the librarian, who fetches it for you.
- If you try to get a referenced book after the library has closed (session closed), you get a LazyInitializationException.
Eager loading is like asking the librarian to bring everything related in one go — you have all the books on your desk immediately, but you might have to carry a heavy load.
Connection to the Next Topic
Lazy loading is a great performance tool, but it can lead to a subtle and devastating problem: the N+1 Query Problem.
- One query to load the parent entities
- N additional queries to load their lazy relationships
This can cripple your application. We’ll explore why it happens, how to detect it, and how to fix it in the next article.
🧠 Quick Challenge for You!
Test your understanding with this scenario:
@Service
public class EmployeeService {
@Transactional(readOnly = true)
public Employee getEmployee(Long id) {
return employeeRepository.findById(id).orElseThrow();
}
}
// In a controller
Employee emp = employeeService.getEmployee(1L);
System.out.println(emp.getDepartment().getName()); // What happens here?
Questions:
- Will this code throw a
LazyInitializationException? Why or why not? - What if we remove
@Transactionalfrom the service method? - How could you fix it without changing the fetch type?
👇 Drop your answers in the comments! This is a classic interview question.
💬 Let’s Discuss!
- Have you ever been bitten by
LazyInitializationException? - Do you prefer lazy or eager loading in your projects?
- Any other Hibernate mysteries you’d like me to demystify?
Leave a comment below — let’s learn together! 👇
🔜 What’s Next
Now that we understand lazy and eager loading, the next article tackles the infamous:
👉**Part 8 — The N+1 Query Problem in Hibernate**
We’ll explore:
- What the N+1 problem is and why it happens
- How to identify it (with real logs)
- Multiple solutions:
JOIN FETCH, entity graphs, and batch fetching - Best practices to avoid performance disasters
📚 Spring Transactions & Hibernate Internals Series
- Part 1 — Transaction Management in Spring Boot
- Part 2 — How @Transactional Works Internally in Spring Boot
- Part 3 — The Self Invocation Problem in Spring Transactions
- Part 4 — Understanding Hibernate Persistence Context
- Part 5 — Dirty Checking in Hibernate
- Part 6 — Flush vs Commit in Hibernate
- Part 7 — Lazy Loading vs Eager Loading in Hibernate 👈you are here
- Part 8 — The N+1 Query Problem in Hibernate
- Part 9 — Interview Q&A
메타데이터
- post_id
- fc92ab33b6dc
- slug
- lazy-loading-vs-eager-loading-in-hibernate-and-why-lazyinitializationexception-happens-fc92ab33b6dc
- url
- https://medium.com/@varuntewani01/lazy-loading-vs-eager-loading-in-hibernate-and-why-lazyinitializationexception-happens-fc92ab33b6dc
- canonical_url
- https://medium.com/@varuntewani01/lazy-loading-vs-eager-loading-in-hibernate-and-why-lazyinitializationexception-happens-fc92ab33b6dc
- author_url
- https://medium.com/@varuntewani01
- status
- ok
- fetched_at
- 2026-06-23 06:34:20