Fix org.hibernate.LazyInitializationException: failed to lazily initialize a collection - no Session

intermediateโ˜• Java2026-03-24| Java 8+, Hibernate 5.x/6.x, Spring Boot 2.x/3.x, JPA, any relational database (MySQL, PostgreSQL, H2)

Error Message

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session
#java#hibernate#jpa#lazy-loading#spring#session

The Error

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session

You fetched an entity inside a transaction, the session closed, and then something tried to access a lazily-loaded relationship. Hibernate can't go back to the database โ€” the session is already gone.

Classic scenario: a service method returns an entity, the transaction ends, and then a controller or JSON serializer touches entity.getOrders(). Boom.

Why This Happens

JPA/Hibernate loads @OneToMany and @ManyToMany collections lazily by default. Hibernate skips fetching them until you actually access them โ€” saves memory, avoids unnecessary queries. The catch: once the Session (or EntityManager) closes, triggering that lazy load throws this exception.

Common triggers:

  • Accessing a lazy collection outside a @Transactional method
  • open-session-in-view disabled โ€” which is the correct production setting
  • Serializing an entity to JSON in the controller layer after the service transaction ended
  • Calling a getter inside a background thread that has no active session

Step-by-Step Fixes

Option 1: Use a Fetch Join in Your Query (Recommended)

One query instead of two โ€” fetch joins tell Hibernate to load the collection alongside the parent entity. No extra round trips, no surprise exceptions later.

// In your repository
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);

Or with JPA Criteria API:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
root.fetch("items", JoinType.LEFT);
query.select(root).where(cb.equal(root.get("id"), orderId));

Load exactly what you need, in one shot, inside the session. Nothing can go wrong after the transaction closes because everything's already loaded.

Option 2: Extend the Transaction Scope with @Transactional

When the lazy access happens in your own service code, keep the session open long enough to initialize what you need.

@Service
public class OrderService {

    @Transactional  // Session stays open for the whole method
    public OrderDTO getOrderWithItems(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new EntityNotFoundException());
        
        // Safe: session is still active here
        order.getItems().size(); // triggers lazy load inside transaction
        
        return toDTO(order); // convert before session closes
    }
}

Convert to DTO inside the transaction โ€” not after. Returning a raw entity and letting the controller touch lazy fields later is exactly what causes the error in the first place.

Option 3: Use DTOs and Stop Exposing Entities

Returning entities directly from service methods is usually where this problem starts. Map to a DTO inside the transaction and you never have to think about lazy loading again โ€” the session can close safely because there's nothing left to load.

@Transactional(readOnly = true)
public OrderDTO findOrder(Long id) {
    Order order = orderRepository.findByIdWithItems(id) // fetch join query
        .orElseThrow(EntityNotFoundException::new);
    
    return new OrderDTO(
        order.getId(),
        order.getStatus(),
        order.getItems().stream()
            .map(item -> new ItemDTO(item.getId(), item.getName(), item.getPrice()))
            .toList()
    );
}

Option 4: Change FetchType to EAGER (Use with Caution)

You can make Hibernate always load the collection:

@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<Item> items;

Simple, but there's a real cost. Every time you load an Order, Hibernate also loads all its items โ€” even when you don't need them. An entity with 3 eager relationships can fire 4 separate queries just to fetch one row. On a list of 50 orders, that's 200+ queries. Use this only for small collections that are always needed.

Option 5: Use Hibernate.initialize() for One-Off Cases

Sometimes you can't restructure the query. In those cases, force initialization explicitly while you're still inside the transaction:

@Transactional
public Order loadOrderWithItems(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    Hibernate.initialize(order.getItems()); // explicit init
    return order;
}

This is a surgical fix. It works, but reach for a fetch join first โ€” it's cleaner and keeps the intent visible in the query.

Option 6: Enable open-in-view (Development Only)

Spring Boot sets spring.jpa.open-in-view=true by default. This keeps the session open for the entire HTTP request, so lazy loads work anywhere in the request cycle. It hides the problem during development, but it holds database connections open far longer than necessary โ€” a real issue under load.

# application.properties
spring.jpa.open-in-view=false  # Disable this in production

Turn it off. You'll see the exception immediately, which forces you to fix the actual problem instead of masking it.

Verification

Run a quick sanity check before shipping:

  • Confirm spring.jpa.open-in-view=false โ€” otherwise you're testing in unrealistic conditions
  • Hit the endpoint that was throwing the exception and check the response
  • Scan application logs for any remaining LazyInitializationException stack traces
  • Enable SQL logging to catch N+1 problems introduced by your fix:
# application.properties
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE

One query per entity in the log means N+1. Switch to a fetch join.

Quick Reference

  • Collection accessed in a loop โ†’ N+1 risk: Use fetch join or @EntityGraph
  • Serialization in controller failing: Convert to DTO inside @Transactional service method
  • Background thread crashing: Open a new transaction explicitly with TransactionTemplate
  • Test environment only: Add @Transactional to your test class to keep the session open during assertions
// EntityGraph alternative to fetch join โ€” keeps repository clean
@EntityGraph(attributePaths = {"items", "items.product"})
Optional<Order> findById(Long id);

The rule is simple: never let a Hibernate entity cross a transaction boundary with uninitialized proxies attached. Decide what data you need, load all of it inside the session, then hand off a plain DTO to the rest of your application.

Related Error Notes