MongoDB Transactions
ACID transactions for multi-document operations
💳 What are Transactions?
MongoDB transactions provide ACID guarantees for multi-document operations. They ensure all operations succeed together or fail together, maintaining data consistency across multiple documents and collections.
// Simple transaction example
const session = client.startSession();
session.startTransaction();
// Perform operations...
await session.commitTransaction();
Result:
All operations committed atomically
Key Transaction Features
Atomicity
All or nothing execution
session.startTransaction()
// All ops succeed or all fail
Rollback
Undo on failure
await session.abortTransaction()
Isolation
Concurrent transaction safety
// Snapshot isolation by default
Commit
Persist all changes
await session.commitTransaction()
🔹 Starting a Transaction
Transactions require a session object. Create a session, start the transaction, perform operations within the session, then commit or abort based on success or failure.
// Basic transaction structure
const { MongoClient } = require('mongodb');
const client = new MongoClient(uri);
async function runTransaction() {
const session = client.startSession();
try {
session.startTransaction();
// Perform operations with session
await collection.insertOne({ name: "Alice" }, { session });
await collection.updateOne(
{ name: "Bob" },
{ $inc: { balance: 100 } },
{ session }
);
// Commit the transaction
await session.commitTransaction();
console.log("Transaction committed");
} catch (error) {
// Abort on error
await session.abortTransaction();
console.log("Transaction aborted");
} finally {
session.endSession();
}
}
Result:
Transaction completed successfully or rolled back on error
🔹 Money Transfer Example
Classic banking scenario demonstrating atomic money transfer between accounts. Both debit and credit operations succeed together or fail together, preventing inconsistent account balances.
// Transfer money between accounts
async function transferMoney(fromAccount, toAccount, amount) {
const session = client.startSession();
try {
session.startTransaction();
// Deduct from sender
const debit = await db.accounts.updateOne(
{ accountId: fromAccount, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);
if (debit.modifiedCount === 0) {
throw new Error("Insufficient funds");
}
// Add to receiver
await db.accounts.updateOne(
{ accountId: toAccount },
{ $inc: { balance: amount } },
{ session }
);
// Record transaction
await db.transactions.insertOne({
from: fromAccount,
to: toAccount,
amount: amount,
date: new Date()
}, { session });
await session.commitTransaction();
console.log("Transfer successful");
} catch (error) {
await session.abortTransaction();
console.log("Transfer failed:", error.message);
} finally {
session.endSession();
}
}
// Usage
await transferMoney("ACC001", "ACC002", 500);
Output:
Money transferred atomically or transaction rolled back
🔹 Multi-Collection Transactions
Transactions can span multiple collections and databases. This ensures consistency across related data stored in different collections, perfect for complex business operations.
// Transaction across multiple collections
async function createOrder(userId, items) {
const session = client.startSession();
try {
session.startTransaction();
// 1. Create order
const order = await db.orders.insertOne({
userId: userId,
items: items,
status: "pending",
createdAt: new Date()
}, { session });
// 2. Update inventory
for (const item of items) {
await db.inventory.updateOne(
{ productId: item.productId, stock: { $gte: item.quantity } },
{ $inc: { stock: -item.quantity } },
{ session }
);
}
// 3. Update user's order history
await db.users.updateOne(
{ _id: userId },
{ $push: { orders: order.insertedId } },
{ session }
);
await session.commitTransaction();
return order.insertedId;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
Result:
Order created, inventory updated, user history modified - all atomically
🔹 Transaction Options
Configure transaction behavior with options like read concern, write concern, and read preference. These settings control data consistency levels and durability guarantees for your transactions.
// Transaction with options
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
readPreference: "primary"
});
// Read Concern Levels:
// - "local": Default, reads latest data
// - "majority": Reads data acknowledged by majority
// - "snapshot": Consistent snapshot across transaction
// Write Concern:
// - w: 1 - Acknowledge from primary only
// - w: "majority" - Acknowledge from majority (recommended)
// Example with options
async function safeTransaction() {
const session = client.startSession();
try {
session.startTransaction({
readConcern: { level: "majority" },
writeConcern: { w: "majority", wtimeout: 5000 },
maxCommitTimeMS: 10000
});
// Perform operations...
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
} finally {
session.endSession();
}
}
Result:
Transaction with custom consistency and durability settings
🔹 Error Handling and Retry
Transactions may fail due to conflicts or timeouts. Implement retry logic with exponential backoff to handle transient errors and ensure eventual success of critical operations.
// Retry logic for transactions
async function runTransactionWithRetry(txnFunc, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
const session = client.startSession();
try {
session.startTransaction();
await txnFunc(session);
await session.commitTransaction();
session.endSession();
return; // Success
} catch (error) {
await session.abortTransaction();
session.endSession();
if (error.hasErrorLabel('TransientTransactionError')) {
attempt++;
console.log(`Retry attempt ${attempt}`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
} else {
throw error; // Non-transient error
}
}
}
throw new Error("Transaction failed after retries");
}
// Usage
await runTransactionWithRetry(async (session) => {
await db.collection.updateOne(
{ _id: 1 },
{ $inc: { count: 1 } },
{ session }
);
});
Result:
Transaction retried automatically on transient failures
🔹 Transaction Best Practices
Follow these guidelines for optimal transaction performance and reliability. Keep transactions short, handle errors properly, and use appropriate consistency levels for your use case.
✅ Do's:
- Keep transactions short and focused
- Use transactions only when necessary
- Always end sessions (use finally block)
- Implement retry logic for transient errors
- Use majority write concern for durability
- Set appropriate timeouts
❌ Don'ts:
- Don't perform long-running operations
- Don't read large amounts of data
- Don't use transactions for single document ops
- Don't forget to handle errors
- Don't nest transactions
// Good transaction pattern
async function goodTransaction() {
const session = client.startSession();
try {
session.startTransaction({
writeConcern: { w: "majority" },
maxCommitTimeMS: 5000
});
// Quick, focused operations
await db.col1.updateOne({...}, {...}, { session });
await db.col2.insertOne({...}, { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession(); // Always cleanup
}
}