Course outline

MongoDB / Intermediate: aggregation and application contracts / Lesson 12

Sessions and multi-document transactions

Choose between single-document atomicity and supported transactions. Define read/write concern, callback retries and external side-effect boundaries explicitly.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain supported transaction topologies and session scope.
  • Design bounded retry-safe business transaction logic.

Use transactions for a real cross-document invariant

Embedding can place related updates in one atomic document, but some business events genuinely span several documents. MongoDB supports multi-document transactions on replica sets from 4.0 and sharded clusters from 4.2; a standalone deployment does not provide that same transaction facility. Transactions do not make a poor unbounded model cheap, and their limits and permitted operations depend on release and topology. Define the invariant, participating documents and failure policy before adding a session around arbitrary requests. Keep external network calls outside the transaction callback.

Pass the same session to every participating operation

This Node.js driver fragment assumes an existing authorized disposable replica-set fixture and an already managed client; neither is created here. The callback uses one session for both writes and checks source funds and destination existence. The chosen read and write concerns are transaction options. The driver may retry supported transaction work, so the callback must not send an email or charge an external gateway. The example remains non-runnable and makes no claim that an engine or driver was executed during authoring.

javascript Illustrative Node.js driver fragment; prerequisites are stated in the explanation.
const session = client.startSession();
try {
  await session.withTransaction(async () => {
    const accounts = client.db('quay_lab').collection('accounts');
    const debit = await accounts.updateOne(
      { _id: 10, balance: { $gte: 15 } }, { $inc: { balance: -15 } }, { session });
    if (debit.matchedCount !== 1) throw new Error('source precondition failed');
    const credit = await accounts.updateOne(
      { _id: 20 }, { $inc: { balance: 15 } }, { session });
    if (credit.matchedCount !== 1) throw new Error('destination missing');
  }, { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
} finally { await session.endSession(); }

With balances 100 and 40 and a successful commit, expected balances are 85 and 55. Either checked failure aborts the intended unit.

Set transaction concern at the right boundary

A session alone does not start a multi-document transaction or automatically provide a snapshot for every operation. Transaction read concern controls its read view, and transaction write concern controls commit acknowledgment; do not set individual operation write concerns inside the transaction. Snapshot reads and majority acknowledgment have documented prerequisites and guarantees that must be considered together. Outside readers using weaker concern may not observe a cross-shard commit as one globally synchronized instant. Choose the actual application read contract rather than reducing ACID to an unqualified marketing label.

A single-document operation can avoid unnecessary transactions

The following update changes reservation quantity and its version in one document under one state predicate. It is already atomic at that boundary and does not need a multi-document transaction merely because two fields change. The expected quantity is six and version three when the starting document has quantity nine and version two. A zero match means the application must reconsider the precondition. If a separate ledger must commit with this update, that additional invariant changes the transaction or model requirement.

javascript Illustrative mongosh; results are reasoned, not measured.
db.getSiblingDB('quay_lab').inventory.updateOne(
  { _id: 4, quantity: { $gte: 3 }, version: 2 },
  { $inc: { quantity: -3, version: 1 } }
);

Single-document atomicity is a modeling tool; it does not coordinate an independently stored ledger automatically.

Retry labels do not replace business identity

TransientTransactionError and UnknownTransactionCommitResult identify different retry situations in supported APIs. Follow the driverโ€™s transaction guidance rather than retrying every exception or only the final statement. An uncertain commit does not prove the transaction failed, and callback retries can execute application code more than once. Persist event identity and a recorded result when a business request may be retransmitted after a lost response. Keep transactions short, avoid parallel operations on one session unless explicitly supported, and clean up sessions on all completion paths.

Common errors and fixes

Transaction numbers are only allowed on a replica set member or mongos

Why it happens
The example targeted an unsupported standalone topology.
How to fix it
Use an explicitly prepared supported disposable topology for independent practice.
Prevent it next time
Verify topology prerequisites before transaction design.

One write escapes the intended transaction

Why it happens
The operation did not receive the participating session.
How to fix it
Pass the same supported session to every operation in the unit.
Prevent it next time
Review driver options and failure tests for each transaction operation.

A retried callback sends duplicate external effects

Why it happens
Non-idempotent side effects were placed inside retryable work.
How to fix it
Use an outbox/idempotency design and separate delivery from database retry.
Prevent it next time
Test callback repetition and uncertain commits explicitly.

Check your understanding

1. A transfer debits a source but the destination is missing. Why must the callback check the credit result?

Show worked solution

An update matching zero documents is not automatically a thrown database error. Without checking it, the callback could commit a debit without the intended credit. Treat missing destination as a failed business precondition and abort the unit. Verify both accounts and the preserved combined balance in a supported isolated transaction fixture; a transaction only coordinates operations actually performed and checked.

2. Why is a session object alone insufficient evidence that two writes are atomic together?

Show worked solution

The application must start a supported transaction, attach the same session to both operations and complete the transaction under appropriate concern settings. Session scope can also be used for causal behavior outside transactions. Inspect the actual driver API and options rather than assuming the presence of a session variable changes every writeโ€™s atomic boundary.

Key takeaways

  • Choose document boundaries first and transactions for the remaining invariant.
  • Session scope, concern and retry-safe callback behavior must agree.

Official references