skip to content

Transactions & Locking

How Hibernate rides database transactions and defends against concurrent writes: optimistic @Version checks, pessimistic row locks, and their interplay with isolation levels. Interviewers pose lost-update scenarios and expect you to pick and justify a locking strategy.

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

explore

questions

25

Using the JPA EntityManager API directly, walk through what happens between EntityTransaction.begin() and EntityTransaction.commit() — in particular, when does Hibernate actually send INSERT, UPDATE and DELETE statements to the database?

level: juniorimportance: must knowfreq 70%

answer

  1. begin = tx marked, auto-commit off
  2. persist queues, IDENTITY is the exception
  3. no SQL until flush
  4. commit = flush then COMMIT
  5. rollback undoes SQL, not Java fields

basics

~20 s

begin() starts one database transaction with JDBC auto-commit off. persist/merge/remove only change in-memory state. commit() first flushes, sending the queued INSERT/UPDATE/DELETE SQL, then issues COMMIT. rollback() sends ROLLBACK and the persistence context must be discarded.

solid answer

~50 s

begin() marks the start of a unit of work; Hibernate ensures the JDBC connection it uses has auto-commit disabled so everything that follows runs in one database transaction. Inside the transaction, persist(), merge() and remove() normally do not hit the database — they record intent in the persistence context. Changes to already-managed entities are not even declared: Hibernate compares each entity against the snapshot taken at load time. SQL is emitted at flush. Flush happens automatically before commit(), before a JPQL/HQL query whose tables overlap the pending changes, and when you call flush() explicitly. So commit() means: flush pending SQL, run before-completion callbacks, send COMMIT, release the connection. rollback() sends ROLLBACK. Statements already flushed are undone in the database, but the Java objects keep their modified field values, so after a rollback the EntityManager must be closed rather than reused. The one common exception to write-behind is IDENTITY id generation, which forces an immediate INSERT inside persist() to obtain the key.

code

java · 14 lines
java
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
    tx.begin();
    Order o = em.find(Order.class, 42L);
    o.setStatus(Status.PAID);        // no SQL yet
    em.persist(new AuditEntry(42L)); // no SQL yet (sequence id)
    tx.commit();                     // flush -> INSERT/UPDATE, then COMMIT
} catch (RuntimeException e) {
    if (tx.isActive()) tx.rollback();
    throw e;
} finally {
    em.close();
}

go deeper

for a junior

Be able to state the sequence: begin, work in memory, flush turns it into SQL, commit ends the database transaction, rollback discards it. Know that changing a managed entity is enough — no explicit save call.

for a middle

Explain write-behind and the three flush triggers, name IDENTITY as the exception that forces an early INSERT, and know that flushed-but-uncommitted SQL is invisible to others.

for a senior

Talk about connection acquisition timing, the cost of holding a transaction open across remote calls, and why the EntityManager must be discarded after a rollback because in-memory state no longer matches the database.

for a principal

Frame transaction boundaries as the unit of consistency and of connection-pool occupancy: boundary width drives pool sizing, lock hold time and retry design, so it is an architectural choice rather than a coding detail.

## Two layers, one boundary Two things are in play. The database transaction is a JDBC connection with auto-commit turned off, ended by COMMIT or ROLLBACK. The persistence context is Hibernate's in-memory unit of work: a map of managed entities keyed by type plus primary key, plus a queue of pending actions. EntityTransaction is the thin bridge between them: begin/commit/rollback on the JPA object translate into operations on the JDBC connection, with a flush inserted before commit. ## begin() em.getTransaction().begin() does not usually open a connection immediately. Modern Hibernate uses delayed connection acquisition: it takes a connection from the pool at the first statement that needs one. What begin() does is mark the session as being in a transaction, so that when a connection is acquired it is used in non-auto-commit mode and kept until completion. Calling begin() twice on the same active transaction throws IllegalStateException. ## Between begin and commit: write-behind Operations queue rather than execute: - persist(entity) makes the instance managed and schedules an insert. If the id generator is a sequence or table generator, Hibernate can allocate the id without touching the row, so the INSERT waits for flush. If the entity uses IDENTITY, the id only exists after the row is written, so Hibernate must execute the INSERT during persist(). - Modifying a managed entity's field schedules nothing explicitly. At flush, dirty checking compares the current field values against the loaded-state snapshot and generates UPDATE only for entities that actually changed. - remove(entity) schedules a DELETE and marks the instance removed. Write-behind exists so Hibernate can batch statements, order them to satisfy foreign keys, and skip work that turns out to be unnecessary (an entity modified and then modified back produces no UPDATE). ## Flush Flush is the act of synchronising the persistence context to the database inside the still-open transaction. It runs at three moments under the default FlushModeType.AUTO: before commit; before executing a query whose query space (the tables it reads) overlaps pending changes, so the query does not miss your own unflushed work; and on an explicit flush() call. Setting FlushModeType.COMMIT removes the query-triggered flush. Flushing is not committing. The SQL is inside the transaction and invisible to other sessions until COMMIT (subject to isolation); a later rollback undoes it. ## commit() commit() flushes, fires before-completion work, then calls Connection.commit(). After that the connection is released back to the pool and the transaction status becomes committed. If flush fails — constraint violation, optimistic-lock failure — commit() does not reach COMMIT; the provider marks the transaction rollback-only and commit() throws RollbackException. After a successful commit the entities stay managed if the EntityManager is still open (application-managed EntityManagers have an extended persistence context), so the next transaction on the same EntityManager continues to see them. ## rollback() rollback() sends ROLLBACK. What it does not do is revert Java objects: an entity whose name you changed still holds the new name, and an entity persisted with a sequence-allocated id still holds that id even though no row exists. The persistence context is now out of sync with the database, which is why the rule is to close the EntityManager after a rollback and build a fresh one to retry. ## Practical shape The canonical resource-local block is: create EntityManager, begin, do work, commit in the try, rollback in the catch when the transaction is still active, close the EntityManager in the finally. Never leave a transaction open while waiting on a remote call — the connection stays checked out and the database sees a long-idle open transaction.

  • If you never call commit and just close the EntityManager, what happens to your changes?
    Nothing is written. Closing the EntityManager with an active resource-local transaction ends the session without flushing, and the connection is returned to the pool, which rolls back any open transaction before reuse. Hibernate does not treat close() as an implicit commit, so silent data loss is the normal outcome of forgetting commit().
  • Does calling flush() make your changes visible to other transactions?
    No. flush() only sends the SQL on your own connection inside the still-open transaction. Other sessions see the rows only after COMMIT, and then only as their isolation level allows. Flushing is useful to force id generation, to surface constraint violations early, or to make a subsequent query see your own pending changes.
  • Why does Hibernate delay writes at all instead of executing each operation immediately?
    Delaying lets Hibernate batch similar statements, order inserts, updates and deletes so foreign keys and unique constraints are satisfied, collapse repeated modifications into a single UPDATE, and skip updates for entities whose fields ended up unchanged. It also shortens the window in which rows are locked by writes.

The persistence context is a shopping cart: adding items changes only the cart. Flush is putting everything on the checkout belt; commit is paying. Walking out (rollback) empties the belt but your notes about what you wanted are still in your hand.

saying these in an interview costs you the question

  • Saying persist() executes an INSERT immediately in all cases, without knowing IDENTITY is the exception
  • Believing flush() commits, or that commit() is needed after flush() only for safety
  • Thinking rollback() restores the previous field values of Java objects
  • Claiming you must call an explicit update or merge for a managed entity to be saved
  • Assuming the connection is held from begin() onward and that this is unavoidable

context

open as a page

In plain JPA/Hibernate, how do you tell the persistence provider to read an entity while holding a database row lock for the rest of the transaction, and what does it actually send to the database?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Pass a pessimistic lock mode on the read: em.find(Order.class, id, LockModeType.PESSIMISTIC_WRITE), query.setLockMode(...), or em.lock(managedEntity, ...). Hibernate emits SELECT ... FOR UPDATE. A transaction must be active, and the database holds the lock until commit or rollback.

open as a page

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%

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.

open as a page

An entity has a field annotated with JPA's @Version. Walk through exactly what SQL Hibernate emits when it flushes a change to that entity, and how it concludes that another transaction modified the row first.

level: middleimportance: must knowfreq 70%

basics

~20 s

Hibernate emits UPDATE t SET cols=?, version = old+1 WHERE id = ? AND version = old. It then reads the JDBC affected-row count. One row means it won; zero rows means someone else already changed the version, so Hibernate raises StaleObjectStateException, surfaced to JPA code as OptimisticLockException.

open as a page

What is the difference between JPA's LockModeType.PESSIMISTIC_READ and LockModeType.PESSIMISTIC_WRITE, and what SQL does Hibernate generate for each?

level: middleimportance: must knowfreq 50%

basics

~20 s

PESSIMISTIC_READ takes a shared lock (SELECT ... FOR SHARE): other readers may also lock it, writers block. PESSIMISTIC_WRITE takes an exclusive lock (SELECT ... FOR UPDATE): nobody else may lock or modify the row. On dialects without shared locks, Hibernate upgrades READ to FOR UPDATE.

open as a page

A legacy table has no version or timestamp column and you are not allowed to alter it. How can Hibernate still detect that another transaction changed a row between your read and your update?

level: middleimportance: must knowfreq 32%

basics

~20 s

Annotate the entity with Hibernate's @OptimisticLocking(type = OptimisticLockType.ALL or DIRTY). Hibernate then puts the originally loaded column values into the UPDATE's WHERE clause; if another transaction changed the row, zero rows match and Hibernate raises a stale-state failure.

open as a page

A call to EntityManager.flush() throws a PersistenceException in the middle of a JPA transaction. What state are the transaction and the EntityManager in afterwards, and what are you allowed to do next?

level: seniorimportance: must knowfreq 52%

basics

~10 s

The provider marks the transaction rollback-only, so any later commit throws RollbackException. The persistence context is now inconsistent with the database. Roll back, discard the EntityManager, and retry the work with a fresh one.

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

A concurrent update makes a flush fail with jakarta.persistence.OptimisticLockException. Describe a retry strategy that is actually correct — what state you must discard, what you must re-read, and when retrying is the wrong answer.

level: seniorimportance: must knowfreq 55%

basics

~20 s

Roll back and discard the persistence context — it holds a snapshot now known to be wrong. Retry the whole transactional unit: open a fresh context, re-read the row, re-apply the business intent (not the stale field values), flush again. Bound the attempts, add jitter, and only retry work that is safe to redo.

open as a page

A pessimistic read in JPA blocks indefinitely while another transaction holds the row. Which standard JPA hint bounds that wait, what special values does Hibernate recognise for it, and which exception surfaces when the wait expires?

level: seniorimportance: must knowfreq 42%

basics

~20 s

The hint jakarta.persistence.lock.timeout, in milliseconds. Hibernate reads 0 as NOWAIT (fail instantly) and -2 as SKIP LOCKED (skip contended rows); -1 waits forever. Expiry raises LockTimeoutException, which — unlike PessimisticLockException — does not mark the transaction for rollback.

open as a page

With a plain JPA EntityManager, do you need to call EntityTransaction.begin() before running a read-only JPQL query, and what actually happens on the JDBC connection if you do not?

level: middleimportance: should knowfreq 45%

basics

~20 s

A single query works without begin(): the connection runs it in its own implicit database transaction and it ends immediately. Multiple reads then see different committed snapshots, and any pending writes are never flushed because no commit happens.

open as a page

A JPA persistence unit can declare transaction-type RESOURCE_LOCAL or JTA. What changes for Hibernate internally and for the code that starts and ends transactions?

level: middleimportance: should knowfreq 42%

basics

~10 s

RESOURCE_LOCAL: Hibernate drives one JDBC connection itself and you call em.getTransaction().begin()/commit(). JTA: an external transaction manager owns the boundary via UserTransaction, em.getTransaction() throws, the EntityManager enlists in the ongoing transaction and flushes at before-completion.

open as a page

JPA's @Version annotation can be placed on an integer-typed field or on a timestamp-typed field. What are the practical differences, and which would you choose for a table written by several application instances?

level: middleimportance: should knowfreq 35%

basics

~20 s

A numeric version is a pure counter: monotonic, database-agnostic, no clock involved, and collisions are impossible. A timestamp version depends on clock resolution and, when generated in the JVM, on clock agreement between instances; column precision can round two updates to the same value. Prefer numeric; use timestamp only when the column must double as a human-readable last-modified.

open as a page

Why is Hibernate's @DynamicUpdate part of the recipe for column-comparison optimistic locking, and what does Hibernate do with UPDATE statements by default without it?

level: middleimportance: should knowfreq 22%

basics

~20 s

By default Hibernate pre-generates one static UPDATE per entity at startup, setting every column. Column-comparison locking needs the statement built per flush — mandatory for DIRTY, whose compared columns vary — and @DynamicUpdate switches Hibernate to that per-flush generation.

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

A user opens an edit form on a record, spends several minutes changing it, and submits. How do you use a JPA @Version field to detect that somebody else changed that row in the meantime, and what common implementation mistake silently defeats the check?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Send the loaded version out with the form, get it back on submit, put it on the detached entity, and merge. Hibernate compares the supplied version with the database row and rejects a stale one. The classic mistake is re-reading the entity server-side and copying only the form fields onto it — the managed copy carries a fresh version, so the check always passes.

open as a page

What does JPA's LockModeType.PESSIMISTIC_FORCE_INCREMENT do that LockModeType.PESSIMISTIC_WRITE does not, and when would you reach for it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

It takes the exclusive row lock and also bumps the entity's version column immediately, in the same operation, even if you never modify the entity. Use it to invalidate other transactions' copies of a parent whose children you are changing. The entity must have a version attribute.

open as a page

Hibernate's @OptimisticLocking annotation accepts OptimisticLockType.ALL and OptimisticLockType.DIRTY. How do the generated UPDATE statements differ, and what concurrency anomaly does DIRTY still allow?

level: seniorimportance: should knowfreq 24%

basics

~20 s

ALL puts every mapped column's loaded value in the UPDATE's WHERE clause; DIRTY puts only the columns this transaction modified. DIRTY therefore lets two transactions edit disjoint columns of the same row concurrently — convenient, but it cannot protect invariants that span columns.

open as a page

A service writes to a relational database through JPA and must also publish a message to a broker for the same business event. How do you decide between running the persistence unit under a JTA transaction manager with two-phase commit and keeping it resource-local with an application-level pattern?

level: principalimportance: should knowfreq 30%

basics

~20 s

Two-phase commit gives real atomicity but adds prepare round trips, a recovery log, in-doubt transactions holding locks, and XA-capable drivers. Most services instead keep one resource-local transaction, write the message as a row in the same commit, and publish it afterwards with at-least-once delivery plus idempotent consumers.

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

Several application instances poll the same jobs table for pending work. Using only JPA/Hibernate locking, how would you design the claim step so no two instances process the same row, and what are the trade-offs of your design?

level: principalimportance: should knowfreq 28%

basics

~20 s

Claim with a limited query using PESSIMISTIC_WRITE plus the SKIP LOCKED timeout value (-2), mark the rows claimed, and commit quickly — then process outside the lock. Trade-offs: no ordering fairness, dialect support required, and crash recovery needs a lease or heartbeat.

open as a page

You must protect a legacy table against lost updates and can either add a dedicated version column or use Hibernate's column-comparison optimistic locking on the existing columns. How do you decide, and what does column comparison fail to protect?

level: principalimportance: should knowfreq 20%

basics

~20 s

Add the version column if you can: it is cheap, indexable, portable and survives detachment. Column comparison is a compatibility fallback for schemas you do not control. Its main gap is detached entities — merge re-reads the row, so conflicts during the detached window go undetected.

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

JPA's jakarta.persistence.lock.scope hint accepts PessimisticLockScope.NORMAL or PessimisticLockScope.EXTENDED. What extra rows does EXTENDED lock, and what does it still leave unprotected?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

NORMAL locks the rows that make up the entity itself, including joined-inheritance and secondary-table rows. EXTENDED additionally locks the rows of join tables and element-collection tables the entity owns, so nobody can add or remove elements. It never locks the associated entities' own rows.

open as a page

You are deciding where to put JPA @Version fields across a domain model. What does versioning every entity cost, and how would you decide which ones actually need it?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

A version column makes conflict detection row-shaped: any two writers to the same row conflict, even on unrelated fields. Version entities with genuinely concurrent writers and lost-update risk. For hot counters and aggregates, change the model — atomic SQL updates or split rows — rather than adding a version and retrying.

open as a page