If you are working with Spring Boot and JPA/Hibernate, chances are you have run into the infamous LazyInitializationException. It usually looks something like this:
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.Entity.collection] - no Session
It is one of the most common exceptions in the Spring ecosystem, but it can be incredibly frustrating if you don’t know why it’s happening.
In this post, we will break down exactly why Hibernate throws this exception and walk through 3 clean, production-ready patterns to fix it permanently.
Why Does LazyInitializationException Occur?
To understand the fix, we first need to understand Hibernate's Lazy Loading strategy.
By default, to optimize performance, Hibernate doesn’t load related entities or collections from the database until you explicitly ask for them. For example, if a User entity has a @OneToMany list of Order entities, Hibernate fetches the User but leaves a "proxy" (a placeholder) for the Orders.
The exception happens when you try to access that proxy after the database transaction (and the Hibernate Session) has already closed.
The Lifecycles of a Request
- Transaction Starts: A controller calls a service method marked with
@Transactional. Hibernate opens aSession. - Data Fetched: The service fetches the
Userentity. Theorderscollection is left as a proxy. - Transaction Ends: The service method finishes. The transaction commits, and Hibernate closes the
Session. - The Crash: Your controller or a mapping library (like Jackson or MapStruct) tries to read
user.getOrders(). Because theSessionis dead, Hibernate cannot fetch the data, and boom—LazyInitializationException.
⚠️ A Quick Warning about OSIV: Spring Boot enables
open-in-view(OSIV) by default, which keeps the Hibernate Session open during the entire web request. While this "hides" the exception during local development, it is a major anti-pattern in production that causes connection pool exhaustion and massive query performance issues. Turn it off in yourapplication.propertieswith:spring.jpa.open-in-view=false.
3 Production-Ready Solutions
Now that we know the cause, let's look at the best ways to solve it without sacrificing performance.
1. Use JPQL JOIN FETCH (The Explicit Approach)
If you know a specific business use case requires the parent entity and its child collections, you can instruct Hibernate to fetch them together in a single database query using JOIN FETCH.
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findUserWithOrdersById(@Param("id") Long id);
}
- Pros: Highly predictable; results in exactly one highly optimized SQL query.
- Cons: You have to write custom JPQL queries for different fetch requirements.
2. Use Spring Data @EntityGraph (The Declarative Approach)
If you prefer to avoid writing manual JPQL queries, Spring Data JPA provides the @EntityGraph annotation. This allows you to dynamically specify which attributes or collections should be eagerly loaded for a specific repository method.
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = {"orders"})
Optional<User> findWithOrdersById(Long id);
}
When you call findWithOrdersById, Hibernate automatically generates the required SQL LEFT OUTER JOIN to bring back both the User and their Orders in one go.
- Pros: Clean, type-safe, and requires zero manual SQL/JPQL.
- Cons: Can become cluttered if an entity has many complex nested relationships.
3. Use DTO Projection (The Clean Architectural Approach)
The cleanest way to avoid LazyInitializationException altogether is to stop exposing database entities directly to your web or API layers. Instead, project your query results directly into a Data Transfer Object (DTO) inside the transaction.
By using Spring Data Projections or a custom JPQL constructor expression, you fetch only the data you need:
// The DTO Record
public record UserOrdersDto(Long id, String username, List<OrderDto> orders) {}
// The Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserOrdersDto(u.id, u.username, o) " +
"FROM User u JOIN u.orders o WHERE u.id = :id")
Optional<UserOrdersDto> findDtoById(@Param("id") Long id);
}
- Pros: Absolute best performance; completely decouples your database schema from your API contract; zero risk of lazy loading issues.
- Cons: Requires creating extra DTO classes and mapping logic.
Summary: Which Solution Should You Choose?
| Scenario | Best Solution | Why? |
|---|---|---|
| Simple queries needing collections | @EntityGraph |
Fast to implement, no custom query strings. |
| Complex queries with multiple joins | JOIN FETCH |
Gives you granular control over the generated SQL. |
| Read-only API endpoints / REST APIs | DTO Projection | Maximizes performance and isolates your entity lifecycle from the view layer. |
By explicitly defining your data requirements within the boundaries of your transactions, you will completely eliminate LazyInitializationException while building a faster, more resilient Spring Boot application.