Transactions & ACID in MongoDB
ACID stands for Atomicity, Consistency, Isolation, and Durability — the guarantees relational databases are famous for. MongoDB provides these at different levels depending on what you are updating.
What ACID means in MongoDB
ACID stands for Atomicity, Consistency, Isolation, and Durability — the guarantees relational databases are famous for. MongoDB provides these at different levels depending on what you are updating.
Atomicity means a group of changes either all succeed or all fail — no half-finished state. Consistency means rules (like stock never going negative) stay true. Isolation keeps concurrent transactions from stepping on each other. Durability means committed data survives crashes.
Real-life example: Transferring money between two bank accounts is ACID. Debit one account and credit the other must happen together — never debit without credit, even if the power fails mid-transfer.
- Single-document updates are always atomic — MongoDB guarantees this on every write.
- Multi-document ACID transactions require a replica set (even a single-node replica set for local dev).
- Transactions add overhead — use only when business logic truly needs all-or-nothing across documents.
- Design with embedding first; reach for transactions when you cannot embed and must keep data consistent.
Single-document atomicity
Every insertOne, updateOne, findOneAndUpdate, or replaceOne on a single document is atomic. MongoDB applies all update operators in one operation — you never get a document with $set applied but $inc skipped.
Embedded arrays and sub-documents inside one document update atomically too. That is why order line items, cart items, and comment threads (when small) belong inside the parent document.
Real-life example: Updating a shopping cart document — adding an item and recalculating total — is one atomic write. The cart never shows a new item with the old total.
// All operators apply atomically on one document
db.carts.updateOne(
{ userId: ObjectId("665a1b2c3d4e5f6789012345") },
{
$push: { items: { productId: ObjectId("..."), qty: 1, price: 899 } },
$inc: { itemCount: 1, subtotal: 899 },
$set: { updatedAt: new Date() }
}
)Multi-document ACID transactions
Since MongoDB 4.0, you can run transactions across multiple documents and collections in a replica set. Start a session, call withTransaction(), pass { session } to every operation, and commit or abort as a unit.
Typical use cases: deduct inventory and create an order, transfer credits between users, update a parent record and an audit log together. Keep transactions short — long-running transactions block other writers and hurt performance.
Real-life example: A movie ticket booking system reserves a seat and charges payment in one transaction. If payment fails, the seat reservation rolls back automatically.
const session = client.startSession();
try {
await session.withTransaction(async () => {
const inventory = db.collection("inventory");
const orders = db.collection("orders");
const result = await inventory.updateOne(
{ sku: "MOUSE-01", stock: { $gte: 2 } },
{ $inc: { stock: -2 }, $set: { updatedAt: new Date() } },
{ session }
);
if (result.modifiedCount !== 1) {
throw new Error("Insufficient stock");
}
await orders.insertOne(
{
sku: "MOUSE-01",
qty: 2,
total: 1798,
status: "confirmed",
createdAt: new Date(),
},
{ session }
);
});
} finally {
await session.endSession();
}await session.withTransaction(
async () => { /* operations */ },
{
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
maxCommitTimeMS: 5000,
}
);Transactions in Mongoose — session.withTransaction
Mongoose wraps the driver session API. Call mongoose.startSession(), then session.withTransaction() with async callback. Pass { session } to create(), updateOne(), save(), and deleteOne() calls inside the callback.
Mongoose models participate in transactions the same way as raw collections. Use try/finally to always end the session.
Real-life example: Mongoose transactions are like a checkout lane supervisor — every item scan (database write) stays on hold until payment clears, then everything finalizes at once.
import mongoose from "mongoose";
const inventorySchema = new mongoose.Schema({
sku: { type: String, unique: true },
stock: { type: Number, min: 0 },
});
const orderSchema = new mongoose.Schema({
sku: String,
qty: Number,
total: Number,
status: String,
}, { timestamps: true });
const Inventory = mongoose.model("Inventory", inventorySchema);
const Order = mongoose.model("Order", orderSchema);
async function placeOrder(sku, qty, unitPrice) {
const session = await mongoose.startSession();
try {
await session.withTransaction(async () => {
const updated = await Inventory.findOneAndUpdate(
{ sku, stock: { $gte: qty } },
{ $inc: { stock: -qty } },
{ session, new: true }
);
if (!updated) throw new Error("Out of stock");
await Order.create(
[{ sku, qty, total: qty * unitPrice, status: "confirmed" }],
{ session }
);
});
} finally {
session.endSession();
}
}When NOT to use transactions
- High-throughput writes where eventual consistency is acceptable (analytics counters, view counts).
- Operations that can be modeled as a single atomic document update.
- Cross-shard transactions — supported but expensive; redesign shard keys or embed data instead.
- Long business workflows — use saga patterns or outbox events for multi-step processes spanning minutes.