skip to content

Tx Boundary & Lazy Loading

The transaction ends before the view or serializer touches a lazy association, producing LazyInitializationException; Open-Session-In-View hides it at a cost, fetching explicitly at the boundary fixes it. Interviewers want the trade-off, not just the exception name.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

What is Open-Session-In-View (OSIV), is it on by default in Spring Boot, and what are its trade-offs?

level: middleimportance: must knowfreq 72%

basics

~20 s

OSIV keeps the Hibernate session open for the entire HTTP request, so lazy loading still works while the response is being serialized. Spring Boot enables it by default. The downside is it holds the database connection longer and can hide N+1 query problems.

open as a page

With OSIV disabled, how do you correctly fetch data at the transaction boundary so serialization never hits a lazy proxy? Compare the options.

level: seniorimportance: should knowfreq 58%

basics

~20 s

Load everything the response needs while the transaction is still open. Use a JOIN FETCH query or an @EntityGraph to populate associations, or map the entity to a DTO inside the @Transactional method. Then the serializer only sees already-loaded data.

open as a page

Explain precisely when the transaction and the Hibernate Session open and close in a Spring MVC request, and how that timing relates to LazyInitializationException with and without OSIV.

level: seniorimportance: should knowfreq 46%

basics

~20 s

Without OSIV: the Session opens when a @Transactional method starts and closes when it commits/returns — before the controller serializes, so lazy access then fails. With OSIV: an interceptor opens the Session at request start and closes it after the response is written, so lazy access during serialization still works.

open as a page

As a tech lead, would you keep OSIV enabled or disable it for a high-throughput REST service? Justify the decision, its risks, and how you'd operationalize the change.

level: principalimportance: should knowfreq 34%

basics

~20 s

For a high-throughput REST API I'd disable OSIV. It frees the database connection sooner, exposes hidden N+1 queries during development, and enforces a clean DTO boundary. The trade-off is more upfront fetching code, which I'd standardize with DTO mapping and fetch tests.

open as a page