Introduction
The LazyInitializationException: could not initialize proxy - no Session is one of the most common errors in Hibernate/Spring Data JPA applications. It occurs when code attempts to access a lazily-loaded association (like @OneToMany or @ManyToOne(fetch = FetchType.LAZY)) after the Hibernate Session has been closed and the entity has become detached. The entity object still exists in memory, but its lazy-loaded collections and references are proxies that require an active session to load the actual data. This error typically appears in the service-to-view layer boundary where entities are returned from a transactional method and accessed outside the transaction scope.
Symptoms
org.hibernate.LazyInitializationException: could not initialize proxy [com.example.entity.User#42] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310)
at com.example.service.UserService.getUserWithOrders(UserService.java:25)Or on collections:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.entity.User.orders, could not initialize proxy - no SessionThe stack trace shows the error originates from a method that is no longer inside a @Transactional boundary.
Common Causes
- Accessing lazy collection outside @Transactional: The transactional method returns the entity, and the caller accesses a lazy association
- Session closed before serialization: Jackson tries to serialize the entity to JSON, triggering lazy loading
- @Transactional on wrong method: The annotation is on an interface method but Spring proxy does not apply it
- Self-invocation bypass: A method within the same class calls another @Transactional method, bypassing the proxy
- Open Session In View enabled: Hides the problem in development but causes performance issues in production
- Detached entity passed to merge: Entity was loaded in one transaction and modified in another without reattachment
Step-by-Step Fix
Step 1: Use JOIN FETCH to eagerly load associations
```java public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT DISTINCT u FROM User u " + "LEFT JOIN FETCH u.orders " + "LEFT JOIN FETCH u.address " + "WHERE u.id = :id") Optional<User> findByIdWithOrdersAndAddress(@Param("id") Long id); } ```
This loads the user, orders, and address in a single SQL query, avoiding lazy loading entirely.
Step 2: Use @EntityGraph for dynamic fetching
```java public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = {"orders", "address"}) Optional<User> findById(Long id); } ```
Or define named entity graphs on the entity:
```java @Entity @NamedEntityGraph( name = "User.withOrders", attributeNodes = @NamedAttributeNode("orders") ) public class User { @Id private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List<Order> orders = new ArrayList<>(); }
public interface UserRepository extends JpaRepository<User, Long> { @EntityGraph(value = "User.withOrders", type = EntityGraph.EntityGraphType.LOAD) Optional<User> findById(Long id); } ```
Step 3: Initialize in the service layer before returning
```java @Service @Transactional(readOnly = true) public class UserService {
private final UserRepository userRepository;
public UserDTO getUserWithOrders(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new EntityNotFoundException("User not found: " + userId));
// Force initialization while still inside @Transactional Hibernate.initialize(user.getOrders()); user.getOrders().size(); // Triggers loading
return UserDTO.fromEntity(user); } } ```
Step 4: Use DTOs to avoid serialization of lazy proxies
```java public record UserDTO( Long id, String name, List<OrderDTO> orders ) { public static UserDTO fromEntity(User user) { return new UserDTO( user.getId(), user.getName(), user.getOrders() != null ? user.getOrders().stream().map(OrderDTO::fromEntity).toList() : List.of() ); } }
public record OrderDTO(Long id, BigDecimal total, LocalDateTime createdAt) { public static OrderDTO fromEntity(Order order) { return new OrderDTO(order.getId(), order.getTotal(), order.getCreatedAt()); } } ```
Prevention
- Never return JPA entities directly from controllers -- always map to DTOs
- Disable
spring.jpa.open-in-viewin production:spring.jpa.open-in-view=false - Use
@EntityGraphfor read operations that need associated data - Use JOIN FETCH for queries that always need specific associations
- Add
Hibernate.isInitialized(entity.getCollection())checks in tests - Configure Jackson to ignore Hibernate proxies:
@JsonIgnoreProperties("hibernateLazyInitializer")