Multi-Document Transactions
When several writes must all succeed together — or all be undone — a transaction guarantees it. Essential for money, orders and anything that must stay consistent.
What you will learn
- Explain what ACID and a transaction guarantee
- Run a transaction with a session: start, commit, abort
- Know when a transaction is needed and when it is overkill
The all-or-nothing problem
Imagine transferring 500 rupees from Asha to Ravi. That is two writes: subtract 500 from Asha, add 500 to Ravi. Now suppose the server crashes between them — Asha has lost 500 and Ravi never got it. The money has vanished. The fix is a transaction: a group of writes that either all succeed or all get undone, never half-done.
Databases describe this guarantee with the word ACID — four promises a transaction makes:
| Letter | Promise |
|---|---|
| A — Atomicity | All writes happen, or none do (no half-finished transfer) |
| C — Consistency | The data obeys its rules before and after |
| I — Isolation | Concurrent transactions do not see each other half-done |
| D — Durability | Once committed, the change survives a crash |
Note: A single document update in MongoDB is already atomic — that one write fully happens or fully fails on its own. You only need a transaction when two or more documents must change as a unit.
Running a transaction
A transaction runs inside a session — think of a session as a bracket around your writes. You start the transaction, do your writes through the session, then commit to make them permanent, or abort to undo them all if anything went wrong. Here it is in Mongoose:
const session = await mongoose.startSession();
session.startTransaction();
try {
await Account.updateOne(
{ name: "Asha" }, { $inc: { balance: -500 } }, { session }
);
await Account.updateOne(
{ name: "Ravi" }, { $inc: { balance: 500 } }, { session }
);
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
} finally {
session.endSession();
}Walking through it. startSession then startTransaction open the bracket. Both updateOne calls pass { session } so MongoDB knows they belong to the same transaction — the first subtracts 500 from Asha ($inc: { balance: -500 }), the second adds 500 to Ravi. If both succeed, commitTransaction makes them permanent together. If either one throws, the catch runs abortTransaction, which rolls back — undoing even the write that did succeed, so the books stay balanced. finally always closes the session to free resources.
Note: Output (on success): both balances change together — Asha 500 lower, Ravi 500 higher. Output (on failure): abortTransaction undoes everything, so NEITHER balance changes. The money is never lost or duplicated.
The lifecycle of every transaction follows the same five steps:
- Start a session with
startSession. - Open the transaction with
startTransaction. - Do each write, passing the
{ session }option so they are part of the transaction. - If all writes succeed,
commitTransactionsaves them together permanently. - If any write throws,
abortTransactionrolls them all back; either way,endSessioncleans up.
Watch out: Transactions cost extra performance and coordination, so use them only when multiple documents must change together — payments, transfers, placing an order that updates stock. For a single-document change, you do not need one; that write is already atomic.
Tip: Transactions require a replica set (covered in the next lesson). MongoDB Atlas runs your free cluster as a replica set already, so transactions work out of the box there.
Q. In a transaction, what does abortTransaction() do if one of the writes fails?
✍️ Practice
- Write a transaction that moves a value between two documents (e.g. transfer points between two users).
- Force the second write to fail and confirm the first write is rolled back.
🏠 Homework
- Model “place an order”: in one transaction, create an order document AND decrease the product’s stock count, aborting if stock would go below zero.