Skip to content

Fix: Hibernate LazyInitializationException — Could Not Initialize Proxy

FixDevs ·

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 Session

Or on a single entity:

org.hibernate.LazyInitializationException:
could not initialize proxy [com.example.Order#42] - no Session

Or in a Spring Boot application:

org.springframework.orm.jpa.JpaSystemException:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection

Or 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 @Transactional method — 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 @Service to @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 DISTINCT in 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 properly

When 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 queries

Fix 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: TRACE

Still 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles