What is a LazyInitializationException, and why does it commonly appear when a controller serializes an entity returned by a service?
answer
- proxy touched, Session already closed
- @Transactional returns → commit → Session closes
- Jackson walks into lazy collection
- DTO inside tx = cleanest fix
- 'could not initialize proxy — no Session'
basics
~10 sIt's a Hibernate error thrown when you access a lazily-loaded field (like a list of orders) after the database session has already closed. The service's transaction ended, so Hibernate can't fetch the data anymore.
solid answer
~40 sLazyInitializationException is thrown by Hibernate when code touches a lazy association (e.g. user.getOrders()) while no open Hibernate Session / persistence context is bound to the thread. The classic path: a @Transactional service loads a User and returns it; the transaction commits and the Session closes on method return. The controller/Jackson then serializes the User and walks into orders, which is a lazy proxy. With no live Session, Hibernate cannot run the SELECT, so it throws. Fixes: fetch what you need inside the transaction (JOIN FETCH, @EntityGraph, or map to a DTO before returning), or keep a Session open across the request (Open-Session-In-View). The DTO approach is preferred because it makes the boundary explicit.
code
java · 27 lines@Entity
class User {
@Id Long id;
// lazy by default for @OneToMany
@OneToMany(mappedBy = "user")
List<Order> orders = new ArrayList<>();
}
@Service
class UserService {
@Transactional
public User findUser(Long id) {
// orders is left as an uninitialized lazy proxy
return userRepository.findById(id).orElseThrow();
} // <-- tx commits, Hibernate Session closes here
}
@RestController
class UserController {
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) {
User u = userService.findUser(id);
// Jackson serializes u -> touches u.getOrders()
// No open Session -> LazyInitializationException
return u;
}
}go deeper
Must recognize the error, name the cause (lazy proxy accessed after Session closed), and know that eager fetching or a DTO fixes it.
Should connect it to transaction/Session lifecycle and mention OSIV as the default that hides it.
Explains why DTO-at-the-boundary beats blanket EAGER and can weigh OSIV trade-offs.
Frames it as a boundary-design concern and sets a team convention for where fetching happens.
## The mechanism When you map a JPA entity with an association like `@OneToMany`, Hibernate defaults collections to **lazy** loading. Instead of the real data, Hibernate hands you a **proxy / lazy collection wrapper**. The real SQL `SELECT` only fires the moment you actually access it (call `getOrders()`, iterate it, etc.). To run that SELECT, Hibernate needs an **open Session** (JPA `EntityManager` / *persistence context*) bound to the current thread. ## Why the exception happens In Spring, the Session lifecycle is tied to the transaction. A typical flow: 1. Controller calls `userService.findUser(id)`. 2. `findUser` is `@Transactional`. Spring opens a transaction and binds a Hibernate Session to the thread. 3. Hibernate loads `User`; `user.orders` is left as an uninitialized lazy proxy. 4. `findUser` returns. Spring **commits the transaction and closes the Session**. 5. Back in the controller, Jackson serializes the `User` to JSON and reaches `orders`. 6. Hibernate tries to initialize the proxy, finds **no open Session**, and throws `org.hibernate.LazyInitializationException: could not initialize proxy — no Session`. The error is a **boundary problem**: data was needed *after* the persistence context that could supply it was gone. ## The three standard fixes - **Fetch eagerly at query time** inside the transaction: `JOIN FETCH` in JPQL, or a `@EntityGraph` on the repository method, so `orders` is already populated before the Session closes. - **Map to a DTO inside the transaction**: pull exactly the fields you need while the Session is open, return a plain DTO. Serialization then touches no proxies. This is the cleanest and most-recommended approach. - **Open-Session-In-View (OSIV)**: keep the Session open for the whole HTTP request so lazy loads still work during serialization. Spring Boot enables this **by default** — convenient but it has real trade-offs (covered in the middle/senior questions). ## Gotchas - `Hibernate.initialize(user.getOrders())` called *inside* the transaction forces the load early — a manual alternative. - Calling `.size()` or iterating in a test outside a transaction reproduces the same error. - Setting `FetchType.EAGER` everywhere "fixes" it but creates performance problems (always-on joins, N+1) — not a real solution.
- Name two ways to fix this without enabling OSIV.Use a fetch join (JOIN FETCH / @EntityGraph) so orders is loaded inside the transaction, or map the entity to a DTO inside the @Transactional method so no proxy is serialized.
- Why doesn't making everything FetchType.EAGER count as a proper fix?It forces joins/extra queries on every load even when the data isn't needed, causes N+1 and cartesian-product problems, and removes control over the fetch — you trade correctness bugs for performance bugs.
saying these in an interview costs you the question
- Thinking the exception is a database connectivity error rather than a closed persistence context
- Believing FetchType.EAGER everywhere is the correct fix
- Assuming the transaction is still open during controller serialization