How does an exception thrown from each of the four callbacks (beforeCommit, beforeCompletion, afterCommit, afterCompletion) affect the transaction outcome and the caller?
answer
- beforeCommit throw = veto + rollback + propagate
- afterCommit throw = propagate but already committed
- beforeCompletion / afterCompletion = swallowed + logged
- only beforeCommit can veto
- wrap afterCommit in try/catch
basics
~10 sbeforeCommit throwing rolls the transaction back and propagates to the caller. beforeCompletion and afterCompletion exceptions are logged and swallowed. afterCommit exceptions propagate to the caller but cannot undo the already-committed transaction.
solid answer
~50 sThe four callbacks differ sharply. beforeCommit runs before the physical commit, so throwing there prevents the commit: Spring rolls back and the exception propagates to the caller — it genuinely changes the outcome. afterCommit runs after a successful commit; an exception there propagates to the caller (the commit call site sees it) but the data is already durable, so it cannot roll anything back — you get a committed transaction plus a thrown exception, which is a nasty inconsistency to design around. beforeCompletion and afterCompletion are 'best-effort cleanup' hooks: Spring catches and logs any exception they throw and continues invoking the remaining synchronizations, so their failures never propagate and never change the outcome. Practical rule: only beforeCommit can veto the commit; afterCommit must be written defensively (catch internally) because it runs post-commit yet still propagates.
code
java · 15 linesTransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override public void beforeCommit(boolean readOnly) {
// Throwing here VETOES the commit -> rollback + propagates.
if (!invariantsHold()) throw new IllegalStateException("abort commit");
}
@Override public void afterCommit() {
// Runs post-commit; an escaping exception propagates but cannot undo.
// So handle failures internally instead of letting them escape.
try {
messagePublisher.publish(event);
} catch (RuntimeException ex) {
deadLetter.record(event, ex); // out-of-band, do NOT rethrow
}
}
});go deeper
Know that afterCommit runs after a durable commit, so it can't undo anything.
State that beforeCommit can veto and completion callbacks swallow exceptions.
Give the full per-callback matrix and the 'committed-but-threw' hazard of afterCommit.
Design post-commit reliability (DLQ/outbox/idempotency) around the fact that afterCommit failures are visible but non-reversible.
## Summary table | Callback | Runs when | Exception effect on transaction | Exception effect on caller | |---|---|---|---| | `beforeCommit` | before physical commit | **rolls back** (commit vetoed) | **propagates** | | `beforeCompletion` | before completion, both paths | none | **swallowed + logged** | | `afterCommit` | after successful commit | none (already committed) | **propagates** | | `afterCompletion` | after completion, both paths | none | **swallowed + logged** | ## beforeCommit — the only veto point Because `beforeCommit` executes while the transaction is still open and before `doCommit`, an exception here causes Spring's `AbstractPlatformTransactionManager.processCommit` to abort the commit and roll back instead. The exception propagates out of the transactional proxy to your caller. This is the *only* callback that can prevent the commit. Use it for last-moment validation that must abort the whole unit of work (though a `@Transactional` service check is usually clearer). ## beforeCompletion — swallowed Spring wraps the invocation and, per the Javadoc, logs any thrown exception and continues. It never propagates and never changes the outcome. It is invoked even after a failed `beforeCommit`. Treat it purely as cleanup; do not rely on it to signal failure. ## afterCommit — the dangerous one `afterCommit` runs *after* the commit succeeded, so the data is durable and nothing can undo it. However, Spring **does propagate** an exception thrown here to the commit caller (the Javadoc states exceptions get propagated to the caller although they do not influence the transaction outcome). Concretely: your DB row is committed, but the caller sees a runtime exception as if the operation failed. This mismatch — 'committed but threw' — is a classic source of bugs (e.g. double-processing on retry). Because of this, code in `afterCommit` (or an `@TransactionalEventListener(AFTER_COMMIT)`) should catch its own exceptions and handle them out-of-band (log, dead-letter, retry queue) rather than letting them escape. ## afterCompletion — swallowed Like `beforeCompletion`, exceptions from `afterCompletion` are caught and logged so that remaining synchronizations still get their `afterCompletion` call. It cannot change the outcome (already completed) and cannot propagate. ## Why the asymmetry Spring's design intent: the *before* pair straddles the commit decision (so `beforeCommit` must be able to veto, while `beforeCompletion` is cleanup that must always run and therefore must not blow up the completion sequence). The *after* pair runs post-outcome; `afterCommit` propagates so that surprising post-commit failures are at least visible to the caller, while `afterCompletion` is the final cleanup pass that must always complete for every synchronization. ## Multiple synchronizations and swallowing For the swallowing callbacks, catching per-synchronization means one bad synchronization does not stop the others from getting their `afterCompletion`/`beforeCompletion`. For `beforeCommit`/`afterCommit`, the first thrown exception stops that phase's iteration and propagates. ## Design guidance - Need to abort the unit of work from a callback → `beforeCommit`. - Post-commit side effect that might fail → `afterCommit`, but wrap in try/catch and route failures to a retry/DLQ; do not let them escape. - Guaranteed cleanup regardless of outcome → `beforeCompletion`/`afterCompletion`; expect exceptions to be swallowed.
- Why is a thrown exception in afterCommit especially dangerous?The transaction is already committed and durable, but Spring still propagates the exception to the caller — the caller thinks it failed, so a naive retry double-processes. Handle failures inside afterCommit.
- Which single callback can prevent the transaction from committing, and why?beforeCommit — it runs before doCommit, so throwing there aborts the commit and triggers a rollback that propagates to the caller.
saying these in an interview costs you the question
- Believing afterCommit can roll the transaction back if it throws
- Thinking beforeCompletion/afterCompletion exceptions propagate to the caller
- Assuming all callbacks behave identically on exception
- Relying on afterCommit exceptions to signal 'operation failed' safely