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?
answer
- Provider marks rollback-only on PersistenceException
- Four survivors: NoResult, NonUniqueResult, LockTimeout, QueryTimeout
- commit() then throws RollbackException
- Failed flush = session unusable, close it
- Retry only transient causes, with a fresh EntityManager
basics
~10 sThe 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.
solid answer
~50 sJPA specifies that the provider marks the current transaction for rollback whenever it throws a PersistenceException, with four documented exceptions that leave the transaction usable: NoResultException, NonUniqueResultException, LockTimeoutException and QueryTimeoutException. A failed flush is not one of those, so the transaction becomes rollback-only. You can confirm it with getRollbackOnly(); calling commit() anyway throws RollbackException and rolls back. The EntityManager is equally damaged. A flush that failed partway can leave some statements executed and others not, identifiers assigned to instances whose rows do not exist, and loaded-state snapshots that no longer match the database. Hibernate's rule is blunt: after such a failure the session must not be reused. So the recovery pattern is: catch, roll back if active, close the EntityManager, and if the failure is transient — a lock timeout, a serialization failure, an optimistic conflict — build a new EntityManager, re-read the data and redo the work from the start.
code
java · 12 linesEntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
em.persist(entity);
em.flush(); // surface constraint failure here, not at commit
em.getTransaction().commit();
} catch (PersistenceException e) {
if (em.getTransaction().isActive()) em.getTransaction().rollback();
throw e; // never reuse em or entity after this
} finally {
em.close();
}go deeper
Know that an exception inside a transaction means the transaction is doomed: roll back and close, do not try to carry on.
Name the rollback-only rule and the four recoverable query exceptions, and explain that commit() then throws RollbackException.
Explain why a partial flush leaves the persistence context inconsistent, show the rollback-close-retry pattern, and classify transient versus deterministic failures for retry policy.
Position retry and idempotency as system-level concerns: bounded retries with backoff around whole units of work, failure classification at the boundary, and avoiding blanket retry wrappers that amplify load during contention.
## Rollback-only as a specification rule EntityTransaction has setRollbackOnly() and getRollbackOnly(). Once the flag is set, the transaction can only end one way: commit() will not commit; it throws RollbackException and performs a rollback. The important part is that you rarely set the flag yourself. The provider sets it. The JPA rule is that throwing any PersistenceException marks the active transaction for rollback, except for four query-level exceptions that are explicitly recoverable: NoResultException (getSingleResult found nothing), NonUniqueResultException (it found several), LockTimeoutException (a lock request timed out but nothing was corrupted) and QueryTimeoutException (the statement exceeded a timeout). Catching those and continuing in the same transaction is legal and normal — for example, catching NoResultException to fall back to creating the entity. Everything else — constraint violations surfaced at flush, optimistic-lock failures, entity-not-found on a required load, JDBC errors — poisons the transaction. ## Why a failed flush also poisons the persistence context Flush executes a batch of statements in a defined order: inserts, updates, collection work, deletes. If the fourth statement violates a unique constraint, the first three already ran. The database will undo them at rollback, but the Java objects do not know that. Concretely you can be left with: - entities holding generated identifiers for rows that will never exist; - loaded-state snapshots that no longer correspond to any row, so subsequent dirty checking computes nonsense; - a partially executed action queue whose remaining entries may or may not have been consumed; - second-level cache entries invalidated for rows that were not actually changed. Hibernate therefore treats the session as unusable. Continuing to work in it, and especially trying to flush again after fixing the offending entity, is the mistake this question is really probing. ## The correct recovery shape Catch the exception, roll back when the transaction is still active, close the EntityManager in a finally block, and decide whether to retry at a level above the persistence context. A retry means: new EntityManager, new transaction, re-read the entities from the database, reapply the business change, commit. You cannot reuse the entity instances from the failed attempt as if they were still valid — merge() on such an object can resurrect stale field values or, worse, carry an identifier that no longer exists. Which failures deserve a retry is a judgement call. Optimistic-lock conflicts, deadlock victims, serialization failures and lock timeouts are transient and worth one or two retries with backoff. Constraint violations caused by bad input are not: retrying reproduces them, so they should surface as an error to the caller. That is why blanket retry-on-any-PersistenceException wrappers are a smell. ## Avoiding the surprise entirely Most people meet this rule as a surprise, because the exception is thrown by commit() rather than by the statement that caused it. Calling flush() explicitly at the point where you expect a constraint to be exercised moves the failure to a place where the stack trace names the operation. It also lets you distinguish this insert violates a unique key from something in this large unit of work failed. Two more details worth knowing. First, checking isActive() before rollback avoids IllegalStateException when the transaction already ended — commit() that threw RollbackException has already rolled back, so calling rollback() afterwards fails. Second, an exception can also arrive from a query rather than a flush, but auto-flush means a query can trigger the very flush that fails, so a query line can throw a constraint violation that has nothing to do with the query. ## Summary rule of thumb Treat a PersistenceException other than the four recoverable query exceptions as terminal for both the transaction and the persistence context. Roll back, close, and retry from a clean slate if the cause is transient.
- Which JPA exceptions do not mark the transaction for rollback, and why those?NoResultException, NonUniqueResultException, LockTimeoutException and QueryTimeoutException. All four are failures of a single read or lock request that leave the persistence context and the database untouched, so continuing the unit of work is safe. Every other PersistenceException may have left partially applied work behind, so the specification forces rollback.
- Why can't you fix the offending entity and call flush() again in the same transaction?Because the flush was partial. Some statements already executed, identifiers may have been assigned for rows that will not survive, and the loaded-state snapshots no longer match the database, so dirty checking produces wrong SQL. Hibernate considers the session unusable after a flush failure; the only safe path is rollback, close, and redo the work in a new persistence context.
- How do you decide whether to retry after such a failure?By classifying the cause. Deadlock victims, lock timeouts, serialization failures and optimistic-lock conflicts are transient contention and are worth a bounded number of retries with backoff, re-reading state each time. Constraint violations from invalid input are deterministic; retrying just reproduces them, so they should be reported to the caller instead.
saying these in an interview costs you the question
- Catching the exception, adjusting a field, and calling flush() again on the same EntityManager
- Assuming commit() will still commit after a failed flush
- Treating every PersistenceException as retryable
- Calling rollback() unconditionally after a commit that threw RollbackException
- Reusing entity instances from the failed attempt via merge() in the retry