skip to content

What is a LazyInitializationException, and why does it commonly appear when a controller serializes an entity returned by a service?

level: juniorimportance: must knowfreq 78%

answer

  1. proxy touched, Session already closed
  2. @Transactional returns → commit → Session closes
  3. Jackson walks into lazy collection
  4. DTO inside tx = cleanest fix
  5. 'could not initialize proxy — no Session'

basics

~10 s

It'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 s

LazyInitializationException 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
java
@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

for a junior

Must recognize the error, name the cause (lazy proxy accessed after Session closed), and know that eager fetching or a DTO fixes it.

for a middle

Should connect it to transaction/Session lifecycle and mention OSIV as the default that hides it.

for a senior

Explains why DTO-at-the-boundary beats blanket EAGER and can weigh OSIV trade-offs.

for a principal

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

context