# Consistency and Transaction Guide Design §12–§16, decisions D-07 through D-10. This is the part of the platform where the wrong default is most expensive and the least visible in testing, because every failure mode here needs a primary change to reproduce. ## 1. Prefer a single-document atomic operation D-09: a transaction is for a **multi-document invariant**, nothing else. A single document is already atomic in MongoDB, so wrapping a one-document update in a transaction buys nothing and costs a session, a two-phase commit and a new ambiguous outcome. D-07: partial change uses update operators, not `save()`. `MongoAtomicOperations` / `MongoAtomicOperationsTemplate` expose the operator set through `MongoUpdateOperator` (`$set`, `$inc`, `$push`, `$pull`, `$addToSet`, `$min`, `$max`, `$currentDate`, …) with an `AtomicFilter` precondition and a `ReturnDocumentMode`. Read-modify-write through `save()` replaces the whole document and silently discards any field another writer changed in between — a lost update with no error. ## 2. Whole-document replacement needs a revision D-08. `VersionedMongoUpdater` requires a `MongoRevision`: either a Spring Data `@Version` field or an explicit expected-revision predicate in `VersionedUpdateCommand`. A replacement whose filter matched zero documents is not "nothing to do" — `MongoOptimisticConflictTranslator` distinguishes: - filter matched nothing and the id does not exist → `MongoDocumentNotFoundException` - filter matched nothing and the id exists → `MongoOptimisticConflictException` Collapsing these two into one is how a concurrent overwrite becomes a 404. ## 3. Consistency profiles `MongoConsistencyProfile` names the read/write concern pair; `MongoConsistencyRegistry` binds a profile to an operation or collection, and `MongoConsistencyBinder` / `ReactiveMongoConsistencyBinder` apply it at execution. | Profile | Meaning | Use for | |---|---|---| | `PRIMARY_LOCAL` | primary read, local concern | Throughput-sensitive reads that tolerate a rollback window. | | `PRIMARY_MAJORITY` | primary read, majority write | The default for anything a user will see again immediately. | | `CAUSAL_MAJORITY` | majority inside a causal session | Read-your-writes across separate operations. | | `STALE_READ_ALLOWED` | secondary reads permitted | Reporting and analytics that state their staleness. | | `SNAPSHOT_TRANSACTION` | snapshot isolation | Multi-document reads inside a transaction. | A profile is a declaration, not a hint: the registry is consulted per operation and an operation without a registered profile is rejected rather than defaulting. ## 4. Causal sessions `MongoCausalSessionContext` plus `SpringMongoCausalSessionExecutor` / `ReactiveMongoCausalSessionExecutor` carry the cluster time and operation time between operations, so "write then read" returns the write even when the read lands on a different node. Without a causal session, `PRIMARY_MAJORITY` gives you durability but not read-your-writes across two calls. In the reactive path the session travels in the Reactor context (`ReactiveMongoContextKeys`), not in a thread local — a thread local is empty on the next operator in the chain. ## 5. Transactions `MongoTransactionExecutor` / `ReactiveMongoTransactionExecutor` open a session through the session factory, run the body, and commit. `MongoTransactionProfile` carries the consistency profile, the `maxCommitTime` and the retry budget. Topology matters: a transaction requires a replica set, and `MongoStartupValidator` refuses a transaction-declaring profile on `STANDALONE` at startup rather than at the first call. ## 6. Retry: body and commit are different loops D-10, and the single most consequential rule in the design. ``` for each body attempt: open a NEW session run the body TransientTransactionError -> abort, next body attempt commit UnknownTransactionCommitResult -> retry COMMIT ONLY, same session ``` `MongoTransactionRetryCoordinator` implements exactly this: - **A new session per body attempt.** Reusing the session after an abort carries the aborted transaction's state into the retry. - **The body is never replayed after a commit ambiguity.** An unknown commit means the commit may already have applied. Re-running the body would apply it a second time. Only the commit is retried, and a commit retry on an already-committed transaction is a no-op by design. - **A budget bounds both loops.** `MongoRetryBudget` limits attempts *and* elapsed time, with jittered backoff (`delayBefore(attempt, random)`), so a struggling primary is not retried into the ground. `MongoRetryDecision` and `MongoRetryScope` (in `…api.error`) say what may be retried: `MongoRetryScope.BODY`, `COMMIT_ONLY`, or `NONE`. ## 7. Ambiguous outcomes `MongoExecutionOutcome` has six values, two of which are ambiguous and must not be collapsed: | Outcome | Did the write happen? | |---|---| | `NOT_SENT` | No. Safe to retry. | | `NO_WRITE_PERFORMED` | No — the server answered and did nothing. | | `WRITE_CONFIRMED` | Yes. | | `PARTIAL_BULK_WRITE` | Some of it. See `MongoBulkResult`. | | `WRITE_RESULT_UNKNOWN` | **Unknown.** | | `TRANSACTION_COMMIT_UNKNOWN` | **Unknown.** | An unknown outcome is not a failure and must not be reported to a caller as one. The caller either reconciles (`MongoCommitReconciler` re-reads a deterministic marker the body wrote) or surfaces the ambiguity. See [runbooks/unknown-commit.md](runbooks/unknown-commit.md). `MongoFailureContext` records only the design-permitted fields — outcome, category, operation name, collection profile, retry scope, attempt — never the query, the document, or the values. ## 8. Failure translation `DefaultMongoFailureClassifier` classifies **labels before codes**. The server's error labels (`TransientTransactionError`, `UnknownTransactionCommitResult`, `RetryableWriteError`) are the authoritative statement about retryability; an error code is a secondary signal whose meaning varies by server version. `DefaultMongoFailureTranslator` maps a classification onto the stable exception hierarchy, and anything unmatched becomes `MongoUnclassifiedFailureException` rather than leaking a driver type. ## 9. Bulk writes `MongoBulkExecutor` returns a `MongoBulkResult` with per-item `MongoBulkItemFailure` entries. An unordered bulk write that partially fails is `PARTIAL_BULK_WRITE`, not a failure: some documents were written. `MongoBulkPartialFailureException` carries the succeeded and failed indexes so a caller can resume rather than replay.