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
@Transactionalmethod open-session-in-viewdisabled โ 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
LazyInitializationExceptionstack 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
@Transactionalservice method - Background thread crashing: Open a new transaction explicitly with
TransactionTemplate - Test environment only: Add
@Transactionalto 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.

