Fix: Hibernate LazyInitializationException — Could Not Initialize Proxy
Part of: Java & JVM Errors
Quick Answer
How to fix Hibernate LazyInitializationException — loading lazy associations outside an active session, fetch join, @Transactional scope, DTO projection, and Open Session in View.
The Error
Accessing a lazily-loaded association outside of a Hibernate session throws:
org.hibernate.LazyInitializationException:
failed to lazily initialize a collection of role: com.example.User.orders,
could not initialize proxy - no SessionOr on a single entity:
org.hibernate.LazyInitializationException:
could not initialize proxy [com.example.Order#42] - no SessionOr in a Spring Boot application:
org.springframework.orm.jpa.JpaSystemException:
org.hibernate.LazyInitializationException: failed to lazily initialize a collectionOr in a test that passes in production but fails without a web context:
LazyInitializationException when accessing user.getOrders()Why This Happens
Hibernate uses lazy loading by default for @OneToMany, @ManyToMany, and @ManyToOne relationships. A lazily-loaded association is a proxy — it doesn’t fetch data from the database until you access it. The proxy requires an active Hibernate session to execute the database query.
The session lifecycle is the key concept. In a Spring Boot application, the Hibernate session is tied to the database transaction. When a @Transactional method returns, the transaction commits and the session closes. Any entity that was loaded inside that transaction still exists in memory, but its lazy proxies are now detached — they have no session to execute queries through. Accessing a lazy property on a detached entity is what triggers the exception.
This is a design trade-off, not a bug. Eager loading everything would eliminate the exception but would cause massive performance problems — loading a single User would recursively load all their Order records, each order’s OrderItem records, each item’s Product, and so on until the entire database is in memory. Lazy loading lets you control exactly what gets fetched, but it requires that all fetching happens within a session boundary.
Common triggers include: entity accessed outside a @Transactional method (the session closes when the transaction ends), Spring’s Open Session in View disabled (OSIV keeps the session open for the entire HTTP request, and disabling it restricts lazy loading to the service layer), returning entities from @Service to @Controller (the controller accessing lazy properties after the transaction closes), serializing entities with Jackson (Jackson accesses all fields, triggering lazy loads after the session is gone), and background threads (async tasks don’t inherit the caller’s Hibernate session).
How Other ORMs and Frameworks Handle This
Hibernate’s LazyInitializationException is specific to JPA-style ORMs that use proxy-based lazy loading. Other data access libraries make fundamentally different trade-offs, and understanding those trade-offs helps you choose the right approach — and explains why some teams abandon Hibernate entirely after fighting this exception one too many times.
EclipseLink (the JPA reference implementation) handles lazy loading differently from Hibernate by default. EclipseLink uses a technique called “weaving” to instrument entity classes at load time, and its lazy proxies can reconnect to a new EntityManager if the original one was closed. This means that some scenarios that throw LazyInitializationException in Hibernate work silently in EclipseLink. However, this reconnection behavior can mask N+1 query problems — code that “works” in EclipseLink might be executing dozens of unintended queries behind the scenes. EclipseLink also defaults to FetchType.EAGER for @ManyToOne and @OneToOne relationships (matching the JPA spec), while Hibernate overrides this to LAZY — which is why migrating from EclipseLink to Hibernate often surfaces LazyInitializationException that didn’t exist before.
MyBatis takes the opposite approach: every SQL query is written explicitly. There are no entity proxies, no automatic lazy loading, and no session-scoped fetching. If you want to load a user’s orders, you write a separate mapper method and call it explicitly. This eliminates LazyInitializationException entirely, but it shifts the burden to the developer to compose queries manually. MyBatis supports a lazyLoadingEnabled setting with association mappings, but it’s rarely used — most MyBatis projects prefer explicit query composition. The trade-off is more boilerplate SQL but complete visibility into what queries are executing.
Spring Data JDBC deliberately avoids lazy loading. Aggregates are loaded fully or not at all. When you load a User entity that contains a List<Order>, all orders are loaded in the initial query. The design philosophy is that each entity is an aggregate root, and loading an aggregate partially leads to inconsistent state. This eliminates the lazy loading exception but forces you to design smaller aggregates — a User with 10,000 orders would be a problem. Spring Data JDBC encourages you to split such cases into separate repository calls.
jOOQ generates type-safe SQL from your database schema. Like MyBatis, it doesn’t have entity proxies or lazy loading. You fetch exactly the columns and joins you specify in the query DSL. This makes N+1 problems impossible by construction — if you only wrote one query, you only execute one query. The downside is that jOOQ doesn’t provide automatic dirty tracking, cascading saves, or the object-relational mapping convenience that Hibernate offers.
The N+1 problem — which is closely related to lazy loading — is handled differently across frameworks. Hibernate’s @BatchSize loads associations in batches (e.g., 25 at a time instead of one at a time). EclipseLink’s @BatchFetch supports IN, JOIN, and EXISTS strategies for batch loading. JPA’s @EntityGraph lets you declaratively specify which associations to fetch per query. MyBatis and jOOQ avoid the problem by requiring explicit joins. The spectrum runs from “fully automatic with hidden costs” (Hibernate) to “fully manual with no surprises” (jOOQ).
Fix 1: Use @Transactional to Keep the Session Open
Ensure all lazy property access happens within a transaction. Annotate the service method with @Transactional and access all needed associations before the method returns:
// Wrong — session closes after findById(), getOrders() fails outside transaction
@Service
public class UserService {
public User getUser(Long id) {
User user = userRepository.findById(id).orElseThrow();
return user; // Session closes here
}
}
@RestController
public class UserController {
public ResponseEntity<?> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
return ResponseEntity.ok(user.getOrders()); // LazyInitializationException!
}
}// Correct — @Transactional keeps the session open throughout the method
@Service
public class UserService {
@Transactional(readOnly = true)
public User getUser(Long id) {
User user = userRepository.findById(id).orElseThrow();
// Access lazy associations INSIDE the transaction
Hibernate.initialize(user.getOrders()); // Force-loads orders
return user;
}
}readOnly = true tells Spring to use a read-only transaction — no dirty checking, faster performance for read operations.
Hibernate.initialize() explicitly initializes a lazy proxy without requiring you to iterate over it:
@Transactional(readOnly = true)
public User getUserWithDetails(Long id) {
User user = userRepository.findById(id).orElseThrow();
Hibernate.initialize(user.getOrders()); // Initialize collection
Hibernate.initialize(user.getAddress()); // Initialize single association
user.getOrders().forEach(o ->
Hibernate.initialize(o.getItems()) // Initialize nested collection
);
return user;
}Fix 2: Use JOIN FETCH to Eager-Load in the Query
Instead of loading the entity and then triggering lazy loads separately, load everything in a single query using JOIN FETCH:
// Spring Data JPA repository
public interface UserRepository extends JpaRepository<User, Long> {
// JOIN FETCH loads orders in the same query — no lazy loading needed
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
// Fetch multiple associations
@Query("SELECT DISTINCT u FROM User u " +
"JOIN FETCH u.orders o " +
"JOIN FETCH o.items " +
"WHERE u.id = :id")
Optional<User> findByIdWithOrdersAndItems(@Param("id") Long id);
}@Service
public class UserService {
@Transactional(readOnly = true)
public User getUserWithOrders(Long id) {
// Single query — no N+1 problem, no lazy loading needed
return userRepository.findByIdWithOrders(id).orElseThrow();
}
}Pro Tip: Use
DISTINCTin the JPQL query when fetching collections to avoid duplicate parent entities in the result. Alternatively, use@QueryHints(@QueryHint(name = HINT_PASS_DISTINCT_THROUGH, value = "false"))to avoid the SQL DISTINCT overhead.
Using JPA Entity Graph:
@Entity
public class User {
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
}
// Repository with named entity graph
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = {"orders", "orders.items"})
Optional<User> findById(Long id);
}Entity graphs are more flexible than JOIN FETCH because they can be defined at the call site without modifying the repository query.
Fix 3: Use DTOs Instead of Entities
The most robust fix is to never return entity objects outside the service layer. Map to DTOs (Data Transfer Objects) inside the transaction, then return the DTO:
// DTO — a plain class, no Hibernate proxy involved
public record UserDTO(
Long id,
String name,
List<OrderDTO> orders
) {}
public record OrderDTO(
Long id,
BigDecimal total,
LocalDate date
) {}
@Service
public class UserService {
@Transactional(readOnly = true)
public UserDTO getUserDTO(Long id) {
User user = userRepository.findByIdWithOrders(id).orElseThrow();
// Map to DTO inside the transaction while session is open
List<OrderDTO> orderDTOs = user.getOrders().stream()
.map(o -> new OrderDTO(o.getId(), o.getTotal(), o.getDate()))
.toList();
return new UserDTO(user.getId(), user.getName(), orderDTOs);
}
}
@RestController
public class UserController {
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
// Receives a DTO — no Hibernate proxies, no lazy loading issues
return ResponseEntity.ok(userService.getUserDTO(id));
}
}JPQL projections — map directly in the query:
public interface UserSummary {
Long getId();
String getName();
Long getOrderCount();
}
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u.id AS id, u.name AS name, COUNT(o) AS orderCount " +
"FROM User u LEFT JOIN u.orders o WHERE u.id = :id GROUP BY u.id, u.name")
Optional<UserSummary> findSummaryById(@Param("id") Long id);
}Fix 4: Fix Jackson Serialization of Lazy Entities
When Spring MVC serializes a JPA entity with Jackson, Jackson tries to access all fields — including lazy collections. If the session is closed, you get LazyInitializationException.
Option A: Exclude lazy fields from serialization:
@Entity
public class User {
@JsonIgnore // Exclude from JSON — prevents Jackson from triggering lazy load
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
}Option B: Use jackson-datatype-hibernate to handle lazy proxies:
<!-- pom.xml -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate6</artifactId>
</dependency>@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer addHibernateModule() {
return builder -> builder.modules(new Hibernate6Module());
}
}Hibernate6Module serializes uninitialized lazy proxies as null instead of throwing. Combined with DTOs, this avoids serialization issues entirely.
Option C: Use DTOs (recommended). Don’t serialize entities directly. Map to DTOs inside a @Transactional service method.
Fix 5: Understand and Configure Open Session in View
Spring Boot enables Open Session in View (OSIV) by default. OSIV keeps the Hibernate session open for the entire HTTP request lifecycle — even after the @Transactional method returns. This allows lazy loading in the view/controller layer but has significant performance downsides (session held open, database connections consumed).
# application.yml
# OSIV enabled (default) — allows lazy loading in controllers
spring:
jpa:
open-in-view: true # Default
# OSIV disabled (recommended for production APIs)
spring:
jpa:
open-in-view: false # Forces you to fix lazy loading properlyWhen you disable OSIV, any lazy loading outside a @Transactional method throws LazyInitializationException. This forces proper design but requires fixing all lazy access issues explicitly.
Why disable OSIV? With OSIV enabled, the database connection is held open from the start of the HTTP request to the end of the response — including during template rendering, JSON serialization, and any other processing. Under load, this can exhaust the connection pool. Disabling OSIV keeps connection usage tight and predictable.
Fix 6: Fix the N+1 Query Problem While You’re Here
Fixing lazy loading often reveals an N+1 query problem: loading a list of 100 users and then accessing user.getOrders() for each one executes 101 queries (1 for users + 1 per user for orders).
// N+1 problem — 1 query for users + N queries for orders
List<User> users = userRepository.findAll();
users.forEach(u -> System.out.println(u.getOrders().size())); // N extra queriesFix with JOIN FETCH:
// 1 query — fetches users and orders together
@Query("SELECT DISTINCT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();Fix with @BatchSize — limits extra queries to batches:
@Entity
public class User {
@BatchSize(size = 25) // Loads orders for 25 users at a time
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Order> orders;
}Enable SQL logging to detect N+1:
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACEStill Not Working?
Check if @Transactional is on the right class. Spring’s @Transactional only works on Spring-managed beans called through a Spring proxy. Calling a @Transactional method from within the same class bypasses the proxy:
// Wrong — self-invocation bypasses the proxy, @Transactional has no effect
@Service
public class UserService {
public void doSomething() {
this.getUser(1L); // Self-invocation — @Transactional is ignored
}
@Transactional
public User getUser(Long id) { ... }
}// Fix — inject self or refactor to a separate bean
@Service
public class UserService {
@Autowired
private UserService self; // Spring injects the proxy, not 'this'
public void doSomething() {
self.getUser(1L); // Goes through the proxy — @Transactional applies
}
@Transactional
public User getUser(Long id) { ... }
}Check the transaction boundary. A @Transactional method on a repository is not enough if the service method calling it isn’t also transactional — the session closes after each repository call:
@Service
public class UserService {
// Each repository call gets its own short transaction
// Session closes between calls — cannot lazy-load between them
public void process(Long id) {
User user = userRepository.findById(id).orElseThrow();
// Session closed here
user.getOrders(); // LazyInitializationException
}
}
// Fix: add @Transactional to the service method
@Transactional(readOnly = true)
public void process(Long id) {
User user = userRepository.findById(id).orElseThrow();
user.getOrders(); // Session still open — works
}Check @Async methods. Spring’s @Async runs the method in a separate thread. The new thread doesn’t inherit the calling thread’s Hibernate session or transaction context. Any entity passed to an @Async method is immediately detached:
@Async
public void processInBackground(User user) {
// user is detached — no session in this thread
user.getOrders(); // LazyInitializationException
}
// Fix: pass the ID, load fresh in the async method
@Async
@Transactional(readOnly = true)
public void processInBackground(Long userId) {
User user = userRepository.findByIdWithOrders(userId).orElseThrow();
user.getOrders(); // Fresh session — works
}Check Hibernate’s enable_lazy_load_no_trans setting. Setting hibernate.enable_lazy_load_no_trans = true opens a temporary session for each lazy load outside a transaction. This eliminates the exception but is considered an anti-pattern — it hides N+1 problems and causes connection pool churn. If you see this setting in your codebase, treat it as technical debt and migrate to explicit fetching strategies.
Verify entity equals/hashCode doesn’t trigger lazy loads. If your entity’s equals() or hashCode() method accesses a lazy collection (e.g., including orders in the hash), putting the entity in a HashSet or calling equals() outside a session triggers the exception. Base equals()/hashCode() on the entity’s ID or natural key, never on lazy associations.
For related Spring issues, see Fix: Spring Boot Bean Creation Exception, Fix: Spring Boot Circular Dependency, Fix: Spring Data JPA Query Not Working, and Fix: Java ConcurrentModificationException.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Spring Boot @Cacheable Not Working — Cache Miss Every Time or Stale Data
How to fix Spring Boot @Cacheable issues — @EnableCaching missing, self-invocation bypass, key generation, TTL configuration, cache eviction, and Caffeine vs Redis setup.
Fix: Spring Data JPA Query Not Working — @Query, Derived Methods, and N+1 Problems
How to fix Spring Data JPA query issues — JPQL vs native SQL, derived method naming, @Modifying for updates, pagination, projections, and LazyInitializationException.
Fix: Spring Boot @Transactional Not Rolling Back — Transaction Committed Despite Exception
How to fix Spring @Transactional not rolling back — checked vs unchecked exceptions, self-invocation proxy bypass, rollbackFor, transaction propagation, and nested transactions.
Fix: Spring Boot Failed to Configure a DataSource
How to fix 'Failed to configure a DataSource: url attribute is not specified' in Spring Boot — adding database properties, excluding DataSource auto-configuration, H2 vs production DB setup, and multi-datasource configuration.