# ADR-MONGO-003 — Transaction body retry and commit retry are separate loops - **Status:** Accepted - **Date:** 2026-08-13 - **Design source:** design §14–§16, decisions D-07 through D-10 ## Context MongoDB reports two transaction failures that look similar and must be handled in opposite ways. `TransientTransactionError` means the transaction definitively did not commit. The correct response is to run the whole thing again. `UnknownTransactionCommitResult` means the commit **may already have applied** — typically because the primary changed while the commit was in flight. The correct response is to retry *the commit*, which is a no-op if it already succeeded. The common implementation wraps everything in one retry loop. That loop replays the body after an unknown commit, and if the commit did apply, the body applies twice. In a payment or notification path that is a duplicate charge or a duplicate message, produced by the error handler. The related trap is session reuse: retrying on the same session after an abort carries the aborted transaction's state into the retry. ## Decision `MongoTransactionRetryCoordinator` implements two loops with different scopes. ``` for each body attempt within the budget: open a NEW session run the body TransientTransactionError -> abort, continue to next body attempt commitWithRetry(session): UnknownTransactionCommitResult -> retry the COMMIT ONLY, same session ``` Rules that follow, all of them load-bearing: 1. **A new session per body attempt.** No aborted state leaks into a retry. 2. **The body is never replayed after a commit ambiguity.** `MongoRetryScope.COMMIT_ONLY` is a distinct value from `BODY` precisely so this cannot be collapsed by accident. 3. **One budget bounds both loops.** `MongoRetryBudget` limits attempts *and* elapsed time, with jittered backoff, so a struggling primary is not retried into the ground by every instance at once. 4. **An exhausted commit retry surfaces `TRANSACTION_COMMIT_UNKNOWN`,** never a generic failure. An ambiguous outcome reported as a failure invites the caller to retry — the one thing that must not happen. See [docs/mongodb/runbooks/unknown-commit.md](../mongodb/runbooks/unknown-commit.md). 5. **Transaction bodies write a deterministic marker** so `MongoCommitReconciler` can establish what actually happened. A transaction that cannot be reconciled has no recovery path. 6. **Classification reads labels before codes.** Server error labels are the authoritative statement about retryability; error codes vary by version. Surrounding decisions that reduce how often this path is reached at all: single-document atomic operations are preferred over transactions (D-09), partial changes use update operators rather than `save()` (D-07), and whole-document replacement requires an optimistic revision (D-08). ## Consequences **Positive.** A commit ambiguity cannot become a duplicate effect. The ambiguity reaches the caller as an ambiguity. The retry budget is bounded in both attempts and time. **Negative.** Callers must handle a third outcome beyond success and failure. Transaction bodies must write a marker they would not otherwise need. Both costs are small compared with reconciling duplicated financial effects after the fact. **Rejected alternative — "one retry loop, at-least-once everywhere."** It requires every transaction body to be fully idempotent, which is a much stronger and much less checkable property than writing one marker, and it is silently violated the first time someone adds a non-idempotent step.