๐ MongoDB Transactions in Node.js โ The Complete Guide with Mongoose
If you think MongoDB isnโt meant for complex, multi-step operations like SQL transactions โ think again.
With replica sets and sharded clusters, MongoDB supports ACID-compliant transactions. And in this blog, Iโll walk you through how to use them in a real-world Node.js backend.
๐ When Do You Even Need Transactions?
Letโs say:
- Youโre deducting money from one user and adding it to another.
- Youโre updating multiple collections together (like orders + inventory).
- Or, you just want to make sure either everything is updated or nothing is.
MongoDBโs default operations are atomic only at the document level. If you need multi-document atomicity, transactions are your tool.
๐งฑ Setup
I'll be using Mongoose here. First, install it:
npm install mongoose
Connect to your database (make sure you're on a replica set):
mongoose.connect("mongodb://localhost:27017/your-db?replicaSet=rs0", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
๐ผ Real-World Example: Wallet Transfer
Letโs build a simple transfer system between two users.
User Schema
const mongoose = require('mongoose');
const { Schema, model } = mongoose;
const userSchema = new Schema({
name: String,
balance: Number,
});
const User = model('User', userSchema);
๐ Transaction Logic
Hereโs how youโd do a transferMoney()
function that updates both sender and receiver in a single transaction:
const transferMoney = async (senderId, receiverId, amount) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const sender = await User.findById(senderId).session(session);
const receiver = await User.findById(receiverId).session(session);
if (!sender || !receiver) throw new Error("User not found");
if (sender.balance < amount) throw new Error("Insufficient balance");
sender.balance -= amount;
receiver.balance += amount;
await sender.save({ session });
await receiver.save({ session });
await session.commitTransaction();
console.log("โ
Transaction committed");
} catch (err) {
await session.abortTransaction();
console.log("โ Transaction aborted:", err.message);
} finally {
session.endSession();
}
};
๐งช Try It Out
(async () => {
await mongoose.connect("mongodb://localhost:27017/your-db?replicaSet=rs0");
const alice = await User.create({ name: "Alice", balance: 100 });
const bob = await User.create({ name: "Bob", balance: 50 });
await transferMoney(alice._id, bob._id, 30);
const updatedAlice = await User.findById(alice._id);
const updatedBob = await User.findById(bob._id);
console.log("Alice's balance:", updatedAlice.balance); // 70
console.log("Bob's balance:", updatedBob.balance); // 80
})();
โ ๏ธ Common Mistakes
- โ Not using a replica set โ transactions won't work on standalone MongoDB.
- โ Forgetting to pass the
session
into all operations inside the transaction. - โ Trying to use transactions in high-throughput scenarios where performance is critical (transactions are slower).
๐ Retry Pattern (Optional but Ideal)
In production, your transaction can fail due to transient network issues. Ideally, wrap your logic in a retry loop with exponential backoff.
You can also check for TransientTransactionError
or UnknownTransactionCommitResult
and reattempt.
โ TL;DR
- Use transactions when you're working with multi-document consistency.
- Always wrap your logic inside
session.startTransaction() / commitTransaction() / abortTransaction()
. - Keep transactions short, simple, and used only when necessary.
Thatโs it! This small trick can prevent big disasters in your backend logic.
If you liked this, Iโve also written about:
๐ Redis beyond SET and GET
๐ How Node.js handles concurrent requests
Let me know if you want a LinkedIn post version or carousel graphic too โ happy to make one!
Top comments (1)
this is extremely impressive the simple breakdown makes me want to actually try mongo transactions for once
you think more folks should default to using transactions, or only keep them for true edge cases