Transactions in MongoDB
🔒 Transactions in MongoDB
$lookup to join data back together: once related data lives in separate documents, a single logical operation can span more than one of them — and MongoDB needs a way to make sure they change together, or not at all. That's what transactions are for.
Why Transactions? The Multi-Document Problem
A write to a single document in MongoDB is already atomic by default — an updateOne() either fully applies or doesn't happen at all, with no built-in transaction needed. That's part of why embedding (Chapter 3, mongodb1-5) is so attractive: an embedded post-with-comments update touches exactly one document, so it's automatically all-or-nothing.
The problem shows up once an operation legitimately needs to change multiple documents — possibly in different collections — as one unit. Chapter 3's comment-count refactor is a direct example: after splitting comments into their own collection, "add a comment" now means two separate writes — inserting the new comment document, and incrementing the post's precomputed comment_count field. Without a transaction, a crash between those two writes leaves the comment saved but the count wrong — the two documents have drifted out of sync.
ACID, Recap
The same four guarantees SQL transactions provide (mysql_foundations_10) apply here: Atomicity (all writes in the transaction succeed, or none do), Consistency (the database moves from one valid state to another), Isolation (concurrent transactions don't see each other's half-finished work), and Durability (once committed, it survives a crash). MongoDB's multi-document transactions genuinely provide all four — this isn't a partial or "best effort" version of ACID.
The Session-Based API
A transaction is tied to a session — every operation that should be part of the transaction has to explicitly run inside that session. The three core steps: startSession(), startTransaction(), then either commitTransaction() on success or abortTransaction() on failure.
Every operation that should be part of the transaction must pass { session } explicitly — an operation that forgets it runs entirely outside the transaction, unprotected by any of its guarantees. If either update throws (say, account A1 doesn't have enough balance and application logic rejects it), abortTransaction() rolls back both writes — the debit never happened, either, even though it ran first and technically "succeeded" on its own.
withTransaction(): The Safer Shorthand
The driver also provides withTransaction(), which wraps the try/commit/catch/abort boilerplate above and automatically retries the whole callback on certain transient errors (see this chapter's closing gotcha) — the recommended approach for production code:
Chapter 3's Comment Refactor, Without vs. With a Transaction
Without a Transaction
Insert the comment, then increment comment_count, as two independent writes. A crash (or thrown error) between them leaves the comment saved but the count under-reporting it — permanently, until something notices and fixes it.
With a Transaction
Both writes run inside one withTransaction() block. Either the comment is inserted and the count is incremented, or neither happens — the two documents can never be observed out of sync with each other.
When You Actually Need Transactions
Skip Transactions When
The operation only touches one document — embedding (Chapter 3) already made this the common case on purpose, and a single-document write is atomic for free, with no transaction overhead at all.
Use Transactions When
An operation must change multiple documents — possibly across collections — and it would be genuinely wrong for some of those writes to succeed while others fail: a funds transfer, an order that must decrement inventory as it's created, a referenced comment-count update.
| Method | Purpose |
|---|---|
startSession() | Creates a session — the container a transaction is scoped to |
startTransaction() | Begins a transaction on that session |
commitTransaction() | Applies every write made in the transaction, atomically |
abortTransaction() | Rolls back every write made in the transaction so far |
withTransaction(fn) | Runs fn inside a transaction, auto-retrying on transient errors, auto-committing/aborting |
💻 Coding Challenges
Challenge 1: Identify the Multi-Document Risk
An e-commerce checkout needs to (a) insert a new order document and (b) decrement the ordered product's stock field in a separate products collection. Explain, in terms of this chapter's ACID recap, what could go wrong without a transaction.
Goal: Practice articulating the specific failure mode (partial, inconsistent writes) a transaction is meant to prevent, not just naming "atomicity" abstractly.
Challenge 2: Write the Checkout Transaction
Using this chapter's withTransaction() pattern, write the session-based code for Challenge 1's checkout: insert the order, then decrement the product's stock, both inside one transaction.
Goal: Practice the full session lifecycle — startSession(), passing { session } to every operation, and endSession() in a finally block.
Challenge 3: Transaction or Not?
Decide whether each of these needs a transaction, and justify why: (a) updating a single user's last_login timestamp, (b) moving a task between two different users' embedded tasks arrays inside their own user documents, (c) transferring loyalty points between two separate customer_accounts documents.
Goal: Practice applying the "single document vs. multiple documents" test directly, including the trickier case (b) where two different top-level documents are each individually atomic on their own.
Multi-document transactions only work against a replica set (or sharded cluster) — a standalone single-node MongoDB instance cannot run them at all, which surprises developers testing locally against a bare mongod. Transactions also carry real performance overhead compared to independent single-document writes, and can hit transient errors (network blips, transaction conflicts) that are expected to be retried, not treated as hard failures — which is exactly why withTransaction(), with its built-in retry logic, is the recommended API over hand-rolling startTransaction/commitTransaction yourself. Keep transactions short and touching as few documents as reasonably possible — they are the exception for cross-document atomicity, not a default way to write MongoDB code.
🎯 What's Next
The next chapter is Replication — the replica sets that transactions turn out to require, how MongoDB elects a primary, automatic failover, and how read preference lets an application choose where its reads come from.