skip to content

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%

answer

  1. tx ≠ Session lifecycle
  2. no OSIV: Session closes on method return, before Jackson
  3. OSIV: interceptor opens at request start, closes after render
  4. OSIV keeps Session open, not transaction (auto-commit loads)
  5. another thread = no bound Session even with OSIV

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.

solid answer

~40 s

In Spring, the persistence context (Hibernate Session / EntityManager) lifecycle is bound to the thread, but by what depends on OSIV. Without OSIV, PlatformTransactionManager opens a transaction and Session when a @Transactional boundary is entered and closes both on commit/rollback when the method returns. So during controller return and Jackson serialization there is no Session — touching a lazy proxy throws LazyInitializationException. With OSIV, OpenEntityManagerInViewInterceptor binds an EntityManager at the very start of the request (before the controller) and unbinds it only after view rendering completes; the service transaction still opens and commits inside that window, but the Session stays open afterward, so lazy loads during serialization succeed — running in auto-commit outside a transaction. Key nuance: OSIV keeps the Session open, not the transaction; and only the transactional portion gets ACID guarantees.

code

java · 24 lines
java
// Demonstrates the timing difference. With open-in-view=false this throws;
// with open-in-view=true it succeeds (via auto-commit lazy load during serialization).
@RestController
class UserController {
    private final UserService service;
    UserController(UserService service) { this.service = service; }

    @GetMapping("/users/{id}")
    public UserResponse get(@PathVariable Long id) {
        User u = service.find(id);           // tx BEGIN..COMMIT, Session closes here if OSIV off
        return UserResponse.from(u);          // touches u.getOrders() -> depends on Session being open
    }
}

@Service
class UserService {
    private final UserRepository repo;
    UserService(UserRepository repo) { this.repo = repo; }

    @Transactional(readOnly = true)          // Session bound to this tx (or reused from OSIV interceptor)
    public User find(Long id) {
        return repo.findById(id).orElseThrow();
    }
}

go deeper

for a junior

Knows the Session closes when the service method returns and that's why serialization fails.

for a middle

Can sketch the with/without-OSIV timelines and where the failure window is.

for a senior

Separates transaction vs Session lifecycles, explains auto-commit post-commit loads and the thread-bound nature of OSIV.

for a principal

Uses the timing model to argue for tx-boundary == fetch-boundary and codifies it (DTOs + OSIV off + fail-fast tests).

## Two independent lifecycles: transaction vs Session It's easy to conflate them, but they are distinct: - **Transaction**: managed by `PlatformTransactionManager` (`JpaTransactionManager` for JPA). Provides atomicity/isolation; delimited by `@Transactional`. - **Persistence context / Session** (`EntityManager` / Hibernate `Session`): the first-level cache and the thing that can initialize lazy proxies. Bound to the thread via `TransactionSynchronizationManager`. Lazy initialization needs an **open Session**. It does **not** strictly need an open transaction — which is exactly why OSIV can serve lazy loads after the transaction has committed. ## Timeline WITHOUT OSIV (`open-in-view=false`) ``` request in DispatcherServlet -> Controller.handler() | |-- calls userService.find(id) @Transactional | [tx BEGIN] [Session OPEN] | load User (orders = lazy proxy) | [tx COMMIT] [Session CLOSE] <-- on method return | |-- controller returns User HttpMessageConverter (Jackson) serializes User -> touches user.getOrders() -> NO Session -> LazyInitializationException response out ``` The Session is gone **before** serialization. This is the failure window. ## Timeline WITH OSIV (`open-in-view=true`, the Boot default) ``` request in OpenEntityManagerInViewInterceptor.preHandle() [Session OPEN + bound to thread] <-- no transaction yet Controller.handler() |-- userService.find(id) @Transactional | [tx BEGIN] (reuses the already-open Session) | load User | [tx COMMIT] <-- Session stays OPEN (owned by the interceptor) |-- returns User Jackson serializes -> user.getOrders() -> Session still open -> SELECT runs (AUTO-COMMIT, no tx) -> works OpenEntityManagerInViewInterceptor.afterCompletion() [Session CLOSE] <-- after response fully rendered response out ``` The interceptor owns the Session for the whole request; the service transaction is just a nested transactional episode within that open Session. ## Critical nuances 1. **OSIV keeps the *Session* open, not the *transaction*.** After the service commits, lazy loads during rendering run in **auto-commit** — each is its own connection round trip, non-transactional, non-atomic. 2. **N+1 is invisible under OSIV**: serializing a collection where each element lazily loads a child fires N extra auto-commit queries with no error. 3. **Connection holding**: depending on Hibernate's connection-release mode and driver, the DB connection may be held for the whole request, reducing pool throughput under load. 4. **Rollback semantics**: a lazy load during serialization can't participate in the service transaction's rollback — the atomic unit of work already ended at commit. 5. **`readOnly=true`** on the service transaction sets the Session to `FlushMode.MANUAL`, skipping dirty-checking — good for read paths regardless of OSIV. 6. **Non-web / async / @Async / new threads**: OSIV binds to the request thread only. Work handed to another thread has no bound Session, so lazy loads fail there even with OSIV on. ## Practical implication The safest design is to make the **transaction boundary == the fetch boundary**: load exactly what the response needs inside `@Transactional`, return DTOs, and disable OSIV so the framework fails fast when you forget. Then the Session-close timing never matters for serialization.

  • Does OSIV keep a transaction open for the whole request?
    No — it keeps the EntityManager/Session open. The service's @Transactional block still begins and commits inside that window; lazy loads that happen after commit (during serialization) run in auto-commit mode, non-transactionally.
  • Why can lazy loading still fail even with OSIV enabled?
    OSIV binds the Session to the request thread only. If work runs on another thread (@Async, manual executor, reactive/parallel stream), that thread has no bound Session, so accessing a proxy there throws regardless of OSIV.

saying these in an interview costs you the question

  • Saying OSIV keeps a database transaction open across the request
  • Believing lazy initialization requires an open transaction (it requires an open Session)
  • Assuming the Session is thread-shared so @Async work can lazy-load under OSIV
  • Not knowing post-commit lazy loads run in auto-commit

context