In production you see 'UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only'. How do you diagnose the root cause?
answer
- stack trace = commit site, not culprit
- enable TransactionInterceptor DEBUG
- 'marking existing transaction as rollback-only' log
- hunt swallowed RuntimeException in try/catch
- watch DataIntegrityViolation / optimistic-lock
basics
~20 sLook for an inner @Transactional method that threw a RuntimeException which some outer method caught and swallowed. The swallowed exception marked the shared transaction rollback-only; find where it was caught by tracing the transactional call chain and logs.
solid answer
~40 sThe message means the commit was vetoed by a rollback-only flag set earlier. Diagnosis: (1) The UnexpectedRollbackException stack trace points to the commit site (the outer transactional boundary), not the real culprit — so it alone won't tell you the cause. (2) Look for an inner @Transactional method (propagation REQUIRED) that threw a RuntimeException whose exception was caught/swallowed by an outer method instead of propagating. (3) Enable TRACE/DEBUG logging on org.springframework.transaction.interceptor and AbstractPlatformTransactionManager to see 'Participating transaction failed - marking existing transaction as rollback-only' and which method set the flag. (4) Search the transactional call chain for try/catch blocks around inner transactional calls. The fix follows: stop swallowing, or isolate with REQUIRES_NEW. Also verify it isn't self-invocation masking the real boundary.
code
yaml · 8 lines# application.yml — reveal which participant marks the tx rollback-only
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.transaction.interceptor.TransactionInterceptor: TRACE
org.springframework.orm.jpa.JpaTransactionManager: DEBUG
# Look for: 'Participating transaction failed - marking existing transaction as rollback-only'
# and the preceding exception + method name -> that is the real root cause.go deeper
Know to look for a caught/swallowed inner exception as the cause.
Enable transaction interceptor logging and trace the call chain to the participant that set rollback-only.
Correlate repository exceptions (integrity/optimistic-lock) and self-invocation with the flag, and reproduce with a test.
Institute logging/alerting and boundary conventions so poisoned-transaction incidents are caught in review/observability, not production.
## What the message tells you (and doesn't) 'Transaction silently rolled back because it has been marked as rollback-only' tells you a commit was attempted on a doomed transaction. The `UnexpectedRollbackException` is thrown at the **commit site** — the outer/owning `@Transactional` boundary — which is usually **not** where the real error happened. So the top of the stack trace misleads; the true culprit is an earlier participant that set the flag and whose exception was swallowed. ## Step-by-step diagnosis **1. Identify the owning boundary.** The stack trace's transactional frame (the `@Transactional` method that was committing) tells you which transaction was doomed. That's your search scope. **2. Turn on transaction logging.** Set these loggers to DEBUG/TRACE: - `org.springframework.transaction.interceptor.TransactionInterceptor` - `org.springframework.orm.jpa.JpaTransactionManager` (or `org.springframework.jdbc.datasource.DataSourceTransactionManager`) - `org.springframework.transaction` broadly. You'll see lines like *'Participating transaction failed - marking existing transaction as rollback-only'* and *'Initiating transaction rollback'* — these pinpoint **which participant** set the flag and **when**. **3. Trace the call chain for swallowed exceptions.** Within the owning boundary, find inner `@Transactional` (REQUIRED) calls wrapped in `try/catch` that log-and-continue. That catch is the bug: the inner interceptor already marked the transaction rollback-only before re-throwing, and the catch hid it. **4. Check the exception type.** Only `RuntimeException`/`Error` mark rollback-only by default. If the inner call threw a checked exception, look for a `rollbackFor` that widened the rule. Conversely a `DataIntegrityViolationException` or optimistic-locking `ObjectOptimisticLockingFailureException` from a repository save is a very common hidden trigger. **5. Rule out self-invocation confusion.** If an inner `@Transactional` seems to have 'no effect', a same-class `this.` call may be bypassing the proxy, changing where the real boundary is. **6. Reproduce with a test.** Write an integration test that triggers the inner failure and asserts the outer commit throws `UnexpectedRollbackException`, then verify the fix removes it. ## Common root causes checklist - try/catch around a repository `save`/`flush` that throws `DataIntegrityViolationException`. - Batch loop catching per-item failures inside one transaction. - A validation or lookup helper annotated `@Transactional` that throws and is caught. - Optimistic locking failure swallowed and retried in-place within the same transaction. ## The fix, once found - Stop swallowing — let the exception propagate for all-or-nothing units. - Isolate the sub-operation with `Propagation.REQUIRES_NEW` (separate bean) if it should be independent. - Move the loop/catch outside the transaction so each unit has its own transaction. ## Prevention - Avoid catching exceptions from inner transactional calls unless you also change propagation. - Keep transaction boundaries explicit and shallow. - Add logging/alerting on `UnexpectedRollbackException` so poisoned-transaction incidents surface early.
- Why is the UnexpectedRollbackException stack trace often unhelpful for finding the cause?It's thrown at the commit site on the outer boundary, not where the failure occurred. The real culprit is an earlier participant that set rollback-only and whose exception was swallowed, so you must trace the call chain and enable transaction logging.
- Which repository-layer exceptions commonly trigger this without an obvious throw in your code?DataIntegrityViolationException (constraint violations) and ObjectOptimisticLockingFailureException from a save/flush. If caught and swallowed inside the transaction, they mark it rollback-only and cause the commit to fail.
saying these in an interview costs you the question
- Debugging only the UnexpectedRollbackException stack trace and ignoring the swallowed original exception.
- Assuming the error originates at the commit method shown on top of the trace.
- Not enabling transaction-level logging to find the participant that set the flag.