Articles
Hibernate Interview Questions: Persistence Contexts, Lazy Loading, and Lost Updates
Practice Hibernate interview questions with one order-editing case covering persistence contexts, lazy loading, N+1 queries, detached merges, and lost updates.
The strongest Hibernate interview answer follows one entity through three boundaries: which persistence context currently manages it, which SQL runs when a lazy association is accessed, and which version is checked when an edit reaches the database. Memorizing annotations alone will not explain why an API returns stale data, throws a lazy-initialization error, or silently overwrites a colleague's change.
This guide uses an original order-editing case to make those boundaries visible. For adjacent concurrency practice, try Implement an Idempotent Versioned Database Update. That question is related preparation, not a claim about any employer's Hibernate round. Technical behavior below is grounded in the Hibernate ORM 7.1 documentation; the interview prompts and sample system are original exercises.
The case: two editors, one order
Imagine an Order entity with id, status, deliveryNote, a lazy customer association, and a numeric @Version. The API has GET /orders/{id} and PATCH /orders/{id}. Alice reads order 41 at version 7; Bob reads the same version. Bob changes the delivery note and commits version 8. Alice later submits a new status from her old screen. The interviewer asks what object state each request has, what happens when customer.name is serialized, and whether Alice can overwrite Bob.
These are separate questions. A persistence context is Hibernate's managed set of entity instances for a unit of work; it tracks identity and changes. A database transaction determines atomic commit and isolation. A client-side copy of an entity or DTO can outlive both. If your answer treats a browser's version-7 payload as though it were still a managed entity in the server's next request, the rest of the concurrency analysis will be wrong.
Question 1: What does a persistence context guarantee?
The Hibernate introduction distinguishes transient, persistent, and detached entities. Within one context, loading the same database identity normally gives the same managed Java object; Hibernate uses that identity map and dirty checking to coordinate changes. The context is not a global cache shared across unrelated requests, and a Session or EntityManager is not thread-safe. It can hold references to managed entities, so long-lived contexts also have memory consequences.
Give an answer tied to the case. In a request-scoped transaction, find(Order.class, 41L) produces a managed instance. If code changes order.setStatus(...), Hibernate can detect that change and issue an update on flush. Once the context closes, the Java object is detached; reading its already-loaded scalar fields is different from trying to initialize an unloaded association. A new request has a new context and must not assume it owns the old instance.
An interviewer may ask whether find twice makes two SQL reads. Within the same active context, an already-managed identity can be returned without another select; a fresh context, an explicit refresh, or altered query/fetch behavior changes the analysis. Phrase this as context-local identity behavior, not a blanket assertion that Hibernate never queries twice.
Question 2: Is flush the same as commit?
No. Flush synchronizes pending entity changes with SQL; commit completes the transaction. A flush may happen before commit, such as before a relevant query or explicit flush(), and the transaction can still roll back. Explaining this difference matters when a test sees an SQL UPDATE in logs yet no committed row afterward. It also matters when an optimistic conflict surfaces during flush rather than only at the final commit call.
For order 41, setStatus("SHIPPED") changes managed state, but that line by itself is not a database transaction. On flush, Hibernate may emit an update constrained by both the primary key and version. Whether another transaction can see that update before commit depends on the database's isolation behavior; do not claim flush makes it visible to everyone. The Hibernate user guide and the database's own transaction documentation are the right references for an implementation-specific answer.
To test this boundary, use a real transaction and an independent reader. A unit test that only inspects the same managed object can pass even when the eventual SQL is wrong. Flush, clear the context, and read from a new context when the question is about persisted state. If you need to observe visibility before commit, use two transactions and make their sequence explicit.
Question 3: Why does customer.name fail after the session closes?
A lazy association is a promise to load related state when needed, subject to mapping and fetch plan. If customer was not initialized and the context needed to load it is closed, dereferencing it can fail with a lazy-initialization exception. The exact behavior can depend on bytecode enhancement, mapping, and whether the association was already initialized; the useful interview answer identifies the access point and session lifetime, not a universal exception for every lazy field.
Suppose the controller returns an Order entity and a serializer later calls order.getCustomer().getName(). If serialization runs after the transaction closes and customer was never fetched, the failure occurs at serialization, not necessarily at the original find. Returning a fully shaped response DTO assembled inside the transaction makes the read contract explicit. A targeted fetch join or entity graph can load the fields needed for that endpoint, as shown in Hibernate's fetch-plan guidance.
Do not solve this by switching every association to eager loading. The Hibernate guide warns that omitted eager associations in a JPQL query can trigger secondary selects. A broad fetch can also load far more data than a list endpoint needs. Name the endpoint's output fields first, then choose a query and verify its SQL count.
Question 4: How do you find an N+1 query in this case?
Imagine a list endpoint returns 20 orders and the response mapper accesses each order's customer.name. If the root query loads orders while customer associations remain unresolved, the mapper may trigger additional selects, potentially one per distinct related customer. The count is not automatically exactly 21: shared customers, batching, first-level identity, caching, and the actual fetch plan all affect it. The invariant is that query count grows with list size when each row provokes a related read.
Measure a small and larger page with SQL logging or Hibernate statistics. Compare the root select, the number of secondary selects, and rows returned. Then choose a query shaped for the endpoint: a DTO projection for a compact list, a selective join fetch or entity graph when the association is genuinely required, or batching when appropriate. Verify the generated SQL and pagination semantics rather than assuming that every fetch join is harmless on a pageable collection.
An answer that says only “use JOIN FETCH” is incomplete. Ask whether the association is to-one or to-many, whether the endpoint needs the full related entity, whether duplicates or cartesian multiplication can occur, and whether the database can page the joined result safely. The trade-off is response data and row cardinality versus extra round trips.
Question 5: Does merge reattach the old object?
The precise answer is that merge copies detached state into a managed instance and returns that managed instance; the argument remains detached. Hibernate's entity-state guidance is the basis for this distinction. If you ignore the return value and keep mutating the old argument, those later mutations are not automatically tracked by the new context.
The more dangerous API mistake is merging a client-shaped Order entity wholesale. Suppose Alice's old payload includes deliveryNote="Leave at side door", while Bob has since changed it to "Use lobby desk". If the server merges all fields from Alice's stale object without an effective version check or field-level policy, Bob's value may be overwritten. Even with a versioned entity, a poorly designed API can discard the client's expected version and freshly load the latest row before applying Alice's stale business intent. Hibernate then sees no persistence-context conflict, because the update starts from the latest version.
Prefer a narrow request DTO: permitted fields plus the version Alice actually saw. Load the managed order in the new transaction, authorize it, compare the submitted expected version to the current version, apply only allowed changes, and handle a concurrent conflict from the final update. That separates mass-assignment safety, user intent, and database concurrency. The exact HTTP status and response are API design choices; explain whether the client should refresh, reconcile, or retry rather than blindly replaying a stale edit.
Question 6: How does @Version stop the lost update?
The Hibernate introduction's optimistic-locking explanation says a version check goes into the WHERE clause of an update or delete for a versioned entity. In this original case, Bob's version-7 update succeeds and advances the row to version 8. Alice's update based on version 7 would affect zero rows, so Hibernate raises an optimistic-lock failure rather than silently replacing Bob's work. The exact SQL column set and exception wrapping depend on mapping and integration.
| Step | Alice | Bob | Stored order |
|---|---|---|---|
| Read | Sees status READY, note A, version 7 | Sees the same snapshot | Version 7 |
| Bob commits | Still holds version-7 intent | Changes note to B using version 7 | Note B, version 8 |
| Alice submits | Requests SHIPPED with expected version 7 | — | Reject stale intent or conflict at version check |
That table distinguishes the client's expected version from the row version in the new server context. A server that loads version 8 and then applies Alice's stale status without comparing her expected version has not solved the user-level lost-update problem merely because the entity has @Version. Conversely, if Alice's request intentionally edits a nonconflicting field and the product supports merging, define that reconciliation policy explicitly; do not silently combine fields by accident.
Question 7: Would a database lock be better?
It depends on the invariant and contention. Optimistic versioning is useful when conflicting writes are uncommon and you can tell a user to refresh or reconcile. A pessimistic lock can serialize competing writers while a transaction holds the row, but adds waiting, deadlock, and lock-duration concerns. The Hibernate locking guide documents both modes and versionless alternatives; it does not make either one universally preferable.
For the order-edit UI, a lock held from Alice's initial browser read until she returns minutes later would be impractical. A short transaction around one inventory decrement might justify a different approach, especially if the invariant spans several rows. Explain the duration of the business decision, the conflict rate, and what happens on failure. Also distinguish the ORM @Version value from the database's own MVCC snapshot or commit version; they are not the same counter.
A strong interview answer in ninety seconds
Start with state: “Alice's browser payload is detached data. The PATCH handler opens a new persistence context and loads a managed order.” Then name the read behavior: “If the response later touches a lazy customer after the context closes, serialization may fail; shape a DTO or fetch the needed relation within the transaction.” Finally handle the write: “Carry Alice's expected version, compare it with the current row, apply only authorized fields, and let the versioned SQL update detect a race between that comparison and flush.”
If asked for tests, propose three: a new-context serialization test, a list-size-versus-query-count test, and a two-transaction concurrency test where Bob commits before Alice flushes. These tests isolate three failure modes. A single mocked repository test would not show when a proxy initializes, which SQL runs, or whether the database rejected a stale version.