DEV Community

Cover image for MongoDB Transactions in Node.js: Complete Guide with Real Examples
Arnav Sharma
Arnav Sharma

Posted on

MongoDB Transactions in Node.js: Complete Guide with Real Examples

๐Ÿ” 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
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ผ 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);
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” 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();
  }
};
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช 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
})();
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ 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)

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

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