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

bash
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:

bash
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.entity.User.orders, could not initialize proxy - no Session

The 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-view in production: spring.jpa.open-in-view=false
  • Use @EntityGraph for 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")