In MongoDB, any operation that modifies a single document is atomic. This means if you update a user document by changing three of its fields, MongoDB guarantees that either all three fields will be updated successfully, or none will be.
But what happens when you need to update multiple documents in a single logical operation?
The Atomicity Problem
Imagine a banking application. Transferring money from Alice's account to Bob's account requires two separate steps:
- Debit: Decrease Alice's balance.
- Credit: Increase Bob's balance.
Let's say we have an accounts collection:
JSON
[
{ "name": "Alice", "balance": 100 },
{ "name": "Bob", "balance": 50 }
]
Our code might look like this: await accounts.updateOne({ name: "Alice" }, { $inc: { balance: -20 } }); await accounts.updateOne({ name: "Bob" }, { $inc: { balance: 20 } });
What happens if the power goes out or the application crashes after the first operation succeeds but before the second one starts? Alice's account has been debited, but Bob's has not been credited. The money has vanished! Your database is now in an inconsistent state.
What are Transactions?
Transactions solve this problem by allowing you to group multiple read and write operations into a single, atomic unit. They provide an "all-or-nothing" guarantee.
- If all operations within the transaction succeed, the changes are saved to the database (this is called a commit).
- If any operation fails, all previous changes made within the transaction are undone (this is called a rollback or abort).
Transactions ensure your data remains consistent, even when complex, multi-step operations are interrupted. They provide the ACID guarantees (Atomicity, Consistency, Isolation, Durability) that are common in relational databases.
Important: Because they add performance overhead, you should only use transactions for operations that genuinely require atomicity across multiple documents or collections.
The Transaction Workflow in Node.js
To use transactions with the official MongoDB Node.js driver, you must use a session. A session represents a group of related operations.
Here's the standard workflow:
- Start a Client Session: Get a session object from your connected client.
- Start the Transaction: Call session.startTransaction().
- Perform Operations: Execute your find, updateOne, insertMany, etc. For each operation, you must pass the session object in the options.
- Commit or Abort:
- If everything in your try block succeeds, call session.commitTransaction() to save the changes.
- If any error occurs, your catch block should call session.abortTransaction() to undo all changes.
- End the Session: In a finally block, always call session.endSession() to clean up.
Prerequisite: Replica Sets
Transactions in MongoDB require a replica set (or a sharded cluster). They will not work on a standalone development instance. MongoDB Atlas clusters are replica sets by default, so they are a great way to get started.
Code Snippet: A Safe Money Transfer
JavaScript
import { MongoClient } from 'mongodb';
const uri = "mongodb://localhost:27017/?replicaSet=rs0"; // Note the replicaSet requirement
const client = new MongoClient(uri);
async function run() {
await client.connect();
const accounts = client.db("banking").collection("accounts");
// Start a new session.
const session = client.startSession();
try {
// Begin transaction
session.startTransaction();
console.log("Transaction started...");
// 1. Debit from Alice's account
await accounts.updateOne(
{ name: "Alice", balance: { $gte: 20 } }, // Ensure Alice has enough funds
{ $inc: { balance: -20 } },
{ session } // Pass the session here
);
// 2. Credit to Bob's account
await accounts.updateOne(
{ name: "Bob" },
{ $inc: { balance: 20 } },
{ session } // And also here
);
// If both operations succeeded, commit the transaction
await session.commitTransaction();
console.log("Transaction committed successfully.");
} catch (error) {
// If anything went wrong, abort the transaction
console.log("An error occurred. Aborting transaction.");
await session.abortTransaction();
throw error; // Rethrow the error
} finally {
// No matter what, end the session
await session.endSession();
await client.close();
console.log("Session ended.");
}
}
run().catch(console.dir);