Writing operations that change several documents across collections atomically with sessions and multi-document transactions, understanding when transactions are needed, how to commit and abort, and the duration limits and best practices for wise usage.

For years, the biggest criticism of MongoDB was "it doesn't support transactions". A single operation was indeed atomic, but multi-document operations couldn't be combined into one unit that succeeds or fails entirely. Everything changed in MongoDB 4.0: multi-document ACID transactions officially arrived, matching the transaction capabilities of relational databases.
Episode 14 covers this feature in depth. The roadmap: first we understand when transactions are truly needed, second we write multi-document transactions with sessions, third we learn how to commit and abort, and fourth we dissect the limitations and best practices. Let's get started.
Transactions guarantee ACID: Atomicity (all succeed or none do), Consistency (data is valid after the transaction), Isolation (transactions don't interfere with each other), and Durability (changes are stored permanently). In MongoDB, a single operation on a single document is always atomic. What needs a transaction is an operation that must change several documents across collections simultaneously.
The most classic example is a balance transfer between accounts. The process requires two inseparable steps: deduct the sender's balance, add to the receiver's balance. If only the deduction succeeds, money is lost; if only the addition succeeds, money is created out of thin air:
{ "_id": ObjectId("aaaa"), "name": "Siti", "balance": 500000 }
{ "_id": ObjectId("bbbb"), "name": "Budi", "balance": 250000 }Without a transaction, a failure mid-way leaves corrupted data. With a transaction, both updates run as one unit — if either fails, both are rolled back.
Other common transaction scenarios: creating an order while decreasing product stock (we'll build this in episode 20), moving documents between collections, or changing several related documents that must stay consistent.
MongoDB transactions run inside a session. All operations in a transaction must pass the same session object — this is what binds those operations into a single transaction:
const session = client.startSession();
session.startTransaction();
try {
await accounts.updateOne(
{ _id: fromAccountId },
{ $inc: { balance: -amount } },
{ session }
);
await accounts.updateOne(
{ _id: toAccountId },
{ $inc: { balance: amount } },
{ session }
);
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
} finally {
await session.endSession();
}Notice the flow:
client.startSession() — creates a session.session.startTransaction() — starts the transaction.updateOne calls receive { session } — the operations are bound to the same transaction.session.commitTransaction() — if everything succeeds, save permanently.session.abortTransaction() — if there's an error, roll back all changes.session.endSession() — clean up the session's resources.The safety key: the transaction guarantees both updates commit together or not at all. There's no halfway state.
Transactions can also be tested directly in mongosh. This is the fastest way to practice the concept before writing application code:
const session = db.getMongo().startSession();
session.startTransaction();
const accounts = session.getDatabase("bank").getCollection("accounts");
accounts.updateOne({ _id: ObjectId("aaaa") }, { $inc: { balance: -100000 } });
accounts.updateOne({ _id: ObjectId("bbbb") }, { $inc: { balance: 100000 } });
session.commitTransaction();
session.endSession();In mongosh, you can test failure scenarios: run a wrong update then session.abortTransaction() — and check that the balance hasn't changed at all. This is the clearest proof of a transaction's power.
transactionLifetimeLimitSeconds). Transactions running longer than that will be aborted automatically by the server. Don't put heavy work or external calls inside a transaction.maxTransactionLockRequestTimeoutMillis and the oplog size. Giant transactions can exceed capacity.system.profile collection inside a transaction.A transaction is a powerful tool, but not a replacement for good schema design:
Keep transactions short. All heavy database work — reading big data, computing — do it before starting the transaction. Inside the transaction, only the operations that must be atomic.
Don't call external APIs inside a transaction. A slow network call burns through the 60-second duration and locks resources. Finish all external work outside the transaction.
Handle retries correctly. commitTransaction can fail due to transient network errors — and it might actually have committed already. Use a retry pattern on the whole block (retryable writes) to be safe from double operations.
Don't make transactions the default habit. Every transaction locks resources and adds overhead. If your operation only changes one document, you don't need a transaction. If it can be redesigned to need just one document (e.g. with embedding), that's better. A transaction is a lifesaver for cases that genuinely require it, not a lifestyle.
Warning
The most common mistake of new transaction developers: forgetting to pass { session } to one of the operations. An operation without a session runs outside the transaction and commits separately — destroying the atomicity guarantee. Always check that every database operation in the transaction block carries the same session.
Info
Understand the thinking behind the design: MongoDB makes "per-document atomicity" the default — this enables super-fast single operations without global locks. Multi-document transactions are an opt-in you pay for with overhead. For data that must always be atomically consistent (money, stock), design using embedding so one document = one business unit — like embedding all order items into a single order document. Transactions are only for cases that genuinely cross that unit.
In episode 14 you understood when multi-document transactions are needed — that is, when several documents across collections must change atomically, like a balance transfer between accounts — and wrote transactions with sessions: starting with startTransaction, passing { session } to every operation, then commitTransaction or abortTransaction, and cleaning up the session with endSession. You also learned the limitations — a 60-second maximum duration, the Replica Set requirement, and transaction size — and the best practices: short transactions, no external APIs, and correct retries.
Key takeaways:
commitTransaction saves everything; abortTransaction rolls back everything.In the next episode, episode 15, we build the foundation of high availability: Replica Sets (High Availability & Read Scaling). You'll understand the primary, secondary, and arbiter architecture, automatic failover through elections, read preferences for scaling reads, and configure a three-member replica set. See you in episode 15!