Fix: Hibernate LazyInitializationException — Could Not Initialize Proxy
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 exception occurs when:
- The entity is accessed outside a
@Transactionalmethod — the session closes when the transaction ends. Any lazy access after that throws the exception. - Spring’s Open Session in View is disabled — OSIV keeps the session open for the entire HTTP request. When disabled (recommended for production), lazy loading fails outside the service layer.
- Returning entities from
@Serviceto@Controller— if the transaction closes in the service, the controller accessing lazy properties fails. - Serializing entities with Jackson — Jackson tries to serialize all fields, triggering lazy loading. If the session is already closed, it throws.
- Background threads — async tasks don’t inherit the caller’s Hibernate session.
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 ✓
}For related Spring issues, see Fix: Spring Boot Bean Creation Exception and Fix: Spring Boot Circular Dependency.
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 "The dependencies of some of the beans in the application context form a cycle"
How to fix Spring Boot circular dependency errors — BeanCurrentlyInCreationException, refactoring to break cycles, @Lazy injection, setter injection, and @PostConstruct patterns.
Fix: Spring Security Returning 403 Forbidden Unexpectedly
How to fix Spring Security 403 Forbidden errors — CSRF token missing, incorrect security configuration, method security blocking requests, and how to debug the Spring Security filter chain.
Fix: Spring Boot Failed to Configure DataSource (DataSource Auto-Configuration Error)
How to fix Spring Boot 'Failed to configure a DataSource' errors — missing URL property, driver class not found, connection refused, and how to correctly configure datasource properties for MySQL, PostgreSQL, and H2.
Fix: Spring BeanCreationException: Error creating bean with name
How to fix Spring BeanCreationException error creating bean caused by missing dependencies, circular references, wrong annotations, configuration errors, and constructor issues.