Diagnosing Hibernate LazyInitializationException: Checks and Fixes
A diagnostic path for Hibernate LazyInitializationException: locate the session boundary that closed too early, then pick a fix that matches the finding.
20 Mar 2026, 03:56 UTC

What the exception actually tells you
org.hibernate.LazyInitializationException is thrown when code touches a lazy association — an uninitialized proxy or collection — after the Hibernate Session that owns it has closed. The message typically names the entity and property and says no session was available. This is not a mapping error; it is a lifetime error. The proxy outlived its session.
Two facts make it diagnosable. The stack trace points at the exact accessor that triggered the load, and Hibernate only throws once the session is gone. So the real question is: which session boundary did this access cross?
Cause map: match the throw location to the cause
| Where it throws | Likely cause | What to inspect |
|---|---|---|
| Controller or view, after a service call returns | Session closed at transaction commit | Whether the accessor runs outside the @Transactional method |
| Inside a service method that is not transactional | No session bound to the thread | Whether the repository call opened and closed its own session |
| During JSON serialization | Serializer walks every getter | Whether an entity, rather than a DTO, is returned from the controller |
| On a background or async thread | Sessions are not thread-safe and are not propagated | Whether the entity was passed across threads |
| In a test assertion | Test harness closed the session or rolled back | Whether the assertion runs after the transaction ends |
Ordered checks
- Find the trigger line. Read the stack trace to the first application frame. It is usually a getter such as
user.getOrders(), atoString(), or a serializer call. - Confirm the session is open there. If the line sits outside the transactional method, that is the cause. Watch for self-invocation: calling a
@Transactionalmethod from another method in the same class bypasses the proxy, so no transaction starts. - Inspect the query. If the session is open and it still throws, the association was never fetched. Check for a
join fetch, an entity graph, or a batch-size setting on the collection. - Audit implicit accessors. Logging frameworks,
equals(),hashCode(), and JSON serializers all read getters. Any of them can initialize a proxy you never intended to touch. - Check Open Session in View (OSIV) only if you rely on it. OSIV keeps the session open until rendering. If it was removed deliberately, the correct fix is fetching, not restoring the filter.
Fixes tied to what you found
Found: the access happens after the transaction closes
Move the access inside the boundary, or load the data before returning. Hibernate.initialize() forces a lazy collection to load immediately, while the session is still open.
@Transactional(readOnly = true)
public UserView load(Long id) {
User user = userRepository.findById(id).orElseThrow();
Hibernate.initialize(user.getOrders()); // runs while the session is open
return new UserView(user.getId(), user.getOrders().size());
}
This removes the exception but may add an extra query. Confirm the query count before treating it as done.
Found: the query never fetched the association
Add a fetch join or entity graph for that one use case instead of changing the mapping to EAGER, which affects every query for that entity.
@Query("select u from User u join fetch u.orders where u.id = :id")
Optional<User> findWithOrders(@Param("id") Long id);
Two caveats: a collection fetch join returns duplicate parent rows unless you use distinct or a Set, and fetching two bags in one query can raise MultipleBagFetchException.
Found: the entity is serialized in the view layer
Return a DTO built inside the transaction. This is the most durable fix because it removes the possibility of an accidental lazy load during serialization. If you must serialize entities, configure the serializer to ignore uninitialized proxies rather than forcing them to load.
Found: OSIV was removed or is misconfigured
If you depend on OSIV, verify the filter is registered for all dispatcher types — REQUEST, FORWARD, INCLUDE, and ASYNC. A forward or async dispatch that skips the filter will throw even though normal requests work. Note that OSIV holds a database connection for the whole request, which is why many teams remove it.
Escalation criteria
- The exception appears only under load or only on some requests: suspect connection pool exhaustion interacting with OSIV rather than a missing fetch.
- Adding fetch joins causes
MultipleBagFetchExceptionor a large row-count blowup: stop adding joins and use@BatchSizeor a second query for the collection. - The entity is detached on purpose, for example cached across requests: lazy loading is simply unavailable in that design, so treat it as an architectural decision rather than a bug to patch.
- The exception originates inside a library you do not control: capture the full stack trace plus the entity and property names before changing any mapping.
Verifying the fix
Enable SQL logging and re-run the failing request. In Spring Boot that is typically spring.jpa.show-sql=true; in plain Hibernate, set the org.hibernate.SQL logger to debug. A fetch join should produce one query containing the join. Hibernate.initialize() should produce a second select before the session closes. Neither should produce a query after the session closes.
For query counts, enable Hibernate statistics with hibernate.generate_statistics=true and read sessionFactory.getStatistics(). Compare the query count per request against your expected fetch plan. Statistics add overhead, so keep them off in production.
A regression test can pin the behavior: load the entity, detach it or close the session, then assert that the accessor either works or throws as designed. Asserting the intended outcome prevents a later refactor from silently reintroducing the problem.
Limitations
The behavior described applies to Hibernate ORM 5.x and 6.x with JPA. Exact filter registration, property names, and statistics APIs differ between Spring Boot versions and plain JPA, so confirm details against the documentation for your version. Fetch joins and entity graphs are not interchangeable in every case: entity graphs cannot apply a fetch join to an arbitrary subquery, and fetch joins cannot be combined with pagination on a collection without an in-memory page. Treat any fix that changes query shape as something to measure, not assume.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.