The pattern
Related writes are split across a transaction boundary: one commits, a later step fails, and the partial state survives. The next retry then collides with the leftovers, or users see records for work that failed. The bug is the boundary, not the failing step.
( 01 )Symptoms
How this failure announces itself.
- warningRetries fail with unique-constraint or already-exists errors after an earlier failure.
- warningA failed request leaves database rows behind that show up in lists and reports.
- warningAudit or outbox tables disagree with the primary tables about what happened.
( 02 )First moves
The first ten minutes — establish facts before touching code.
- 1Take one stuck retry and identify the leftover row blocking it; note which table committed and which step failed.
- 2Read the code path and mark each write as inside or outside the transaction - the first outside one is the suspect.
- 3Confirm whether the failing step throws through the transaction helper, or is swallowed before it gets there.
- 4Reproduce by injecting a failure immediately after the first write in a test.
( 03 )Where to look
The code and config that usually owns this bug.
- searchTransaction helper boundaries - which calls are actually inside the callback, and which only look like it?
- searchRepository calls that grab their own connection instead of the transaction's handle.
- searchError paths - does every throw between the writes propagate to a rollback, or do some get caught and logged on the way?
- searchOutbox and audit writes - side-channel writes outside the transaction drift from the primary data.
( 04 )Common fixes
Fix the cause, then make the regression impossible.
- buildMove related writes into one transaction, passing the transaction handle to every repository involved.
- buildLet errors propagate through the transaction helper so rollback actually runs on every thrown path.
- buildUse an outbox pattern for external side effects so they commit atomically with the data they describe.
- buildAdd failure-injection tests that throw between each pair of writes and assert clean rollback.
( 05 )Prove the fix
A fix you can't demonstrate is a guess. Close the loop.
- verifiedFailure-injection tests show zero leftover rows for a failure at any step.
- verifiedThe originally stuck retry path succeeds after a failure instead of colliding with leftovers.
- verifiedOutbox and audit state match primary state across all injected failures.