MongoDB path · Writes you can trust · Topic 9 of 12

Multi-document transactions

Not started

A multi-document transaction groups reads and writes across documents and collections so they commit together or not at all, with snapshot isolation. MongoDB has supported them on replica sets since 4.0 and on sharded clusters since 4.2. They work much as in a relational database, but the trade-off is different: MongoDB is built so that most operations touch one document, which is atomic anyway, and a transaction adds coordination on top of that.

Why it matters: Transactions make some designs possible that used to need awkward workarounds. They are also slower than single-document writes, can abort on conflict, and are not a reason to keep a relational schema in a document database.

  1. 1Single-document writes need no transaction

    One document is always updated atomically. Good schema design keeps most invariants inside one document.

  2. 2Replica sets since 4.0, sharded clusters since 4.2

    A transaction runs in a session, reads a snapshot, and commits all its writes together or none of them.

  3. 3Conflicts abort, so retry

    If two transactions write the same document, one fails with a WriteConflict labelled TransientTransactionError. The callback API retries the whole transaction for you.

  4. 4Keep them short

    By default a transaction is aborted after 60 seconds. Long transactions hold cache and history, and across shards every commit is a two-phase commit.

1

How it works

The explanation, with examples you can run.

A transfer

const session = db.getMongo().startSession()const accounts = session.getDatabase("shop").accountssession.startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } })accounts.updateOne({ _id: "ana" }, { $inc: { balance: -30 } })accounts.updateOne({ _id: "ben" }, { $inc: { balance: 30 } })// another client reads heresession.commitTransaction()
outside, before commit: [{"_id":"ana","balance":100},{"_id":"ben","balance":20}]after commit:           [{"_id":"ana","balance":70},{"_id":"ben","balance":50}]

Readers outside the transaction see the old values and are not blocked. The commit's write concern applies to the whole transaction; operations inside it do not take their own.

Conflicts

Two transactions that write the same document cannot both proceed. The second writer does not wait for the first: it fails at once.

112 WriteConflict ["TransientTransactionError"]

The label tells you the whole transaction can be retried from the start. In application code, use the driver's callback API, which retries on that label and retries the commit on UnknownTransactionCommitResult:

await session.withTransaction(async () => {  await accounts.updateOne({ _id: "ana" }, { $inc: { balance: -30 } }, { session })  await accounts.updateOne({ _id: "ben" }, { $inc: { balance: 30 } }, { session })})

Because the callback can run more than once, keep side effects such as sending email or calling another service out of it.

What it costs

  • Time limit. A transaction running longer than transactionLifetimeLimitSeconds (60 by default) is aborted.
  • Cache pressure. Open transactions keep old versions of data in the WiredTiger cache. Abort abandoned transactions instead of leaving them open.
  • Sharded commits. A transaction that writes to several shards commits with a two-phase protocol coordinated by one shard, adding round trips. Transactions that stay on one shard are cheaper.
  • Concurrency. MongoDB 9.0 caps the number of open transactions (maxConcurrentMultiDocumentTransactions, 10,000 by default) and rejects new ones with TooManyOpenTransactions above it.
  • Oplog. A large transaction is written as several oplog entries; each must still fit the 16 MB document limit.

The manual is direct about it: in most cases a distributed transaction costs more than single-document writes and should not replace effective schema design. Use transactions for invariants that genuinely span documents (a transfer between two accounts, a uniqueness rule across collections, an order plus its outbox event), and embed the rest.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.What do other clients see while a transaction that moved 30 from ana to ben is still open?
  2. 2.A transaction fails with WriteConflict and errorLabels TransientTransactionError. What should the application do?
  3. 3.An order and its lines are in separate collections and every change uses a transaction. What is often the better design?

0 of 3 right so far. Get all three to finish this topic.