skip to content

Isolation Levels & the ORM

Where database isolation and Hibernate's own guarantees meet — the L1 cache already gives repeatable reads of entities, whatever the connection's isolation. Interviewers probe whether you solve lost updates at the database level or with version checks, and why.

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

questions

5

Inside one Hibernate Session you load a row by primary key, another session commits an update to that row, and you load it by the same primary key again — you still see the old field values. Explain why, and say whether the database isolation level is what produced that behaviour.

level: middleimportance: must knowfreq 55%

answer

  1. Identity map keyed by type + id
  2. Second find = no SQL at all
  3. Repeatable read by identity, not by isolation
  4. Query rows fresh, managed entity state kept
  5. refresh / clear / new session to re-read

basics

~20 s

The persistence context is an identity map keyed by entity type plus id. The second lookup returns the instance already in it without re-reading, so you get repeatable reads at application level regardless of the database isolation level. Only refresh, clear or a new session re-reads.

solid answer

~50 s

The first load put the entity in the persistence context, indexed by class and identifier. The second find() for the same identifier is answered from that map — no SQL at all — so the other session's committed change cannot be observed. This is an application-level repeatable read, produced by the ORM, not by the database. It holds even on READ COMMITTED, and it also holds when a query does hit the database: if a row maps to an entity that is already managed, Hibernate discards the freshly read column values and returns the existing instance, because two managed instances of the same row would break the unit-of-work guarantee that the same id yields the same object. The practical consequences are that you cannot refresh state by loading again — you need refresh(), clear() or a new persistence context — and that long-lived sessions can operate on stale data without any error appearing.

code

java · 7 lines
java
Order a = em.find(Order.class, 42L);   // SELECT ...
// another session commits: update orders set status='CANCELLED' where id=42
Order b = em.find(Order.class, 42L);   // no SQL
assert a == b;                          // same instance
// b.getStatus() is still the value loaded first

em.refresh(a);                          // SELECT ... : now current

go deeper

for a junior

Say that the session keeps one object per row and hands the same object back, so nothing is re-read.

for a middle

Frame it as repeatable read by identity provided by the ORM, independent of isolation, and name refresh, clear and a new persistence context as the ways to re-read.

for a senior

Discuss the query asymmetry — fresh rows, retained entity state — and the incident shape of long-lived sessions that make decisions on stale data with no error.

for a principal

Set persistence-context lifetime as an architectural rule: one context per unit of work, explicit refresh points, and a write-time concurrency check wherever read-then-write spans a request.

## The identity map A persistence context holds every managed entity in a map keyed by entity type plus primary key. That map is what people call the first-level cache, and it is not optional or configurable: it is the unit-of-work guarantee that within one session there is exactly one Java object per database row. Code can rely on reference equality, and Hibernate can rely on having one place to record each entity's loaded-state snapshot for dirty checking. That guarantee produces the behaviour in the question. em.find(Order.class, 42L) the second time finds the key present and returns the same instance immediately. No SELECT is issued, so nothing anyone else committed can be seen. ## Why it is not isolation Isolation levels describe what a database transaction may observe when it does read. If the ORM does not read, isolation has nothing to act on. So the observed stability is an ORM property layered on top of whatever the engine offers, and it appears identically under READ COMMITTED, REPEATABLE READ or SERIALIZABLE. The distinction matters in two directions. Upward: do not conclude that your database is giving repeatable reads because your entities look stable — it may not be, and other parts of the same transaction (aggregate queries, projections, native SQL) will show the newer data. Downward: do not conclude that stable means current — you may be making decisions on values that were replaced minutes ago, with no exception to warn you. ## Queries behave differently, and then the same A JPQL or Criteria query always executes SQL; the identity map is keyed by id, not by query text, so it can never answer a query. The rows come back fresh. But then Hibernate resolves each row to an entity: if that identifier is already managed, the newly read column values are thrown away and the existing instance is returned. So a query gives you fresh membership — new rows appear, deleted ones vanish from the result — with possibly stale field values for anything you already had. That asymmetry surprises people who try to refresh state by re-running a query. ## How to get fresh state deliberately - em.refresh(entity) re-reads that row and overwrites the instance's fields, discarding unflushed changes to it. This is the targeted tool. - em.clear() detaches everything; the next load is a real read. Blunt but useful between phases of a long unit of work. - em.detach(entity) does the same for one instance. - A new EntityManager per unit of work is the cleanest answer, and the reason short, request-scoped sessions are the default advice. Note that refresh and reload do not by themselves prevent lost updates; they only narrow the window. Detecting a concurrent modification at write time is a separate mechanism. ## Where teams get hurt The classic incident is a long-lived session — a batch job or a desktop-style conversation — that loads data once and keeps working. Every decision it makes uses the snapshot from when it started, while the world moves on. Nothing fails; the data is just wrong. The fix is boundary discipline: one persistence context per unit of work, closed at the end, plus a concurrency check at write time for anything read then written. A second trap is testing. A test that writes through the same session it later reads from will pass even if the write was never sent to the database, because the identity map returns the in-memory instance. Flushing and clearing, or using a separate persistence context for the assertions, is what makes such a test meaningful. ## The one-line summary The first-level cache gives you repeatable reads by identity inside a session, for free and unconditionally. It says nothing about database isolation and nothing about freshness.

  • If a JPQL query does hit the database, why doesn't it update the fields of an entity you already have?
    Because the persistence context must keep exactly one instance per row, and that instance may already carry unflushed modifications. Overwriting it from the result set would silently discard the application's changes and break reference identity. Hibernate therefore discards the freshly read column values and returns the managed instance; refresh() is the explicit opt-in to the opposite behaviour.
  • How does this affect a test that writes and then reads back the entity?
    The read can be answered from the identity map, so the assertion passes even if the SQL was wrong or never sent. To test the mapping and the write path, flush and clear the persistence context before reading, or perform the read in a separate persistence context, so the data really comes from the database.

The session is a notebook where you copy each record the first time you look it up. Asking again reads your own notebook, not the filing cabinet — steady, but only as current as the moment you copied it.

saying these in an interview costs you the question

  • Explaining the stability by saying the database is in REPEATABLE READ
  • Believing loading the entity again re-reads the row from the database
  • Thinking refresh() and a second find() are equivalent
  • Assuming the first-level cache also answers JPQL queries
  • Treating stable in-memory state as evidence that the data is current

context

open as a page

A request loads an entity, changes a field in memory, and the ORM issues the UPDATE at commit. The database runs at READ COMMITTED isolation. Does that isolation level stop a concurrent request from silently overwriting the change, and if not, what does?

level: seniorimportance: must knowfreq 50%

basics

~20 s

No. READ COMMITTED takes no lock on a plain read, so two sessions can read the same row and the second UPDATE simply overwrites the first — a lost update. You need a version check in the UPDATE's WHERE clause, a locking read, or a single atomic UPDATE statement.

open as a page

The same JPQL query, executed twice inside one persistence context, returns a different number of rows the second time, yet the entities that were already loaded still show their old field values. Explain both halves of that behaviour.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Queries always execute SQL — the first-level cache is keyed by id, not by query — so rows committed by others appear or disappear as the isolation level allows. But rows whose entities are already managed are resolved to the existing instances, and the freshly read column values are discarded.

open as a page

For a high-traffic service that reads and writes through an ORM, how do you decide between raising the database isolation level for all transactions, taking explicit locking reads on the contended rows, and enforcing correctness with an application-level version check?

level: principalimportance: should knowfreq 28%

basics

~20 s

Keep the engine default and apply targeted mechanisms. Raising isolation globally taxes every transaction and forces retry handling everywhere. Version checks suit rare conflicts, locking reads suit hot contended rows, and single atomic statements suit relative updates.

open as a page

What does the Hibernate configuration property hibernate.connection.isolation do, and what are the pitfalls of setting it when connections come from a pool or from a JTA datasource?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

It makes Hibernate call Connection.setTransactionIsolation on connections it obtains, using a java.sql.Connection constant or its symbolic name. It is global to the session factory, not per transaction, and it is the wrong place to set isolation for pooled or JTA-managed connections.

open as a page