MongoDB Multi-Document Transactions
Guarantee consistency with ACID transactions across multiple operations.
Quick Start
Basic Transaction
const session = client.startSession()
try {
await session.withTransaction(async () => {
await users.insertOne({ name: 'John' }, { session })
await accounts.insertOne({ userId: 'xxx', balance: 100 }, { session })
})
} catch (error) {
console.error('Transaction failed:', error)
} finally {
await session.endSession()
}
Real-World: Money Transfer
async function transferMoney(fromId, toId, amount) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
await accounts.updateOne(
{ _id: fromId },
{ $inc: { balance: -amount } },
{ session }
)
await accounts.updateOne(
{ _id: toId },
{ $inc: { balance: amount } },
{ session }
)
})
} catch (error) {
console.error('Transfer failed:', error)
} finally {
await session.endSession()
}
}
Transaction Requirements
MongoDB Version
- MongoDB 4.0+: Single-document transactions (all versions)
- MongoDB 4.0: Multi-document transactions (replica sets)
- MongoDB 4.2+: Multi-document transactions (sharded clusters)
Deployment Type
- Replica Set: Required for transactions
- Sharded Cluster: MongoDB 4.2+
- Standalone: Single-document transactions only
- Atlas Free: No transactions (shared clusters)
Session Management
Create Session
const session = client.startSession()
const session = client.startSession({
defaultTransactionOptions: {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' },
readPreference: 'primary'
}
})
Session Lifecycle
const session = client.startSession()
session.startTransaction()
await collection.insertOne(doc, { session })
await session.commitTransaction()
await session.abortTransaction()
await session.endSession()
Transaction Options
Read Concern
await session.withTransaction(async () => {
}, {
readConcern: { level: 'snapshot' }
})
Write Concern
await session.withTransaction(async () => {
}, {
writeConcern: { w: 'majority' }
})
Read Preference
{
readPreference: 'primary'
}
Error Handling
Handle Transaction Errors
async function robustTransaction() {
const session = client.startSession()
try {
await session.withTransaction(async () => {
})
} catch (error) {
if (error.hasErrorLabel('TransientTransactionError')) {
return robustTransaction()
} else if (error.hasErrorLabel('UnknownTransactionCommitResult')) {
console.log('Commit outcome unknown')
} else {
throw error
}
} finally {
await session.endSession()
}
}
Retry Logic
async function executeWithRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
await fn(session)
})
return
} catch (error) {
if (error.hasErrorLabel('TransientTransactionError') && attempt < maxRetries) {
continue
} else {
throw error
}
} finally {
await session.endSession()
}
}
}
Real-World Examples
Order Placement with Inventory
async function placeOrder(userId, items) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
const order = {
userId,
items,
status: 'pending',
createdAt: new Date()
}
const orderResult = await orders.insertOne(order, { session })
for (const item of items) {
const updated = await products.findOneAndUpdate(
{ _id: item.productId },
{ $inc: { stock: -item.quantity } },
{ session, returnDocument: 'after' }
)
if (updated.value.stock < 0) {
throw new Error('Insufficient inventory')
}
}
const userUpdate = await users.findOneAndUpdate(
{ : userId },
{ : { : -(items) } },
{ session, : }
)
(userUpdate.. < ) {
()
}
orderResult.
})
} (error) {
.(, error.)
error
} {
session.()
}
}
Account Reconciliation
async function reconcileAccounts(mainId, secondaryIds) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
const secondary = await accounts.find(
{ _id: { $in: secondaryIds } },
{ session }
).toArray()
const totalBalance = secondary.reduce((sum, acc) => sum + acc.balance, 0)
await accounts.updateOne(
{ _id: mainId },
{ $set: { balance: totalBalance } },
{ session }
)
await accounts.deleteMany(
{ _id: { $in: secondaryIds } },
{ session }
)
await reconciliationLog.insertOne({
mainId,
secondaryIds,
totalBalance,
timestamp: new Date()
}, { session })
})
} finally {
await session.endSession()
}
}
Limitations & Considerations
Transaction Limits
- Maximum 16MB of write operations
- Cannot create/drop collections
- Cannot create/drop indexes
- Cannot alter collections
- Cannot write to system collections
Performance Impact
- Transactions have overhead
- Write operations slower (more coordination)
- Locking increases lock contention
- Not for every operation
Best Practices
✅ Transaction Best Practices:
- Keep short - Minimize lock time
- Retry transient errors - Network issues happen
- Order operations - Prevent deadlocks
- Use appropriate write concern - 'majority' for safety
- Monitor latency - Transactions add overhead
✅ When to Use:
- ✅ Money transfers
- ✅ Order processing
- ✅ Inventory management
- ✅ Account operations
- ✅ Any multi-collection atomic operations
❌ When NOT to Use:
- ❌ Single document operations (inherently atomic)
- ❌ Simple inserts/updates
- ❌ Performance-critical reads
- ❌ Batch operations (use bulk)
- ❌ If not on replica set
Next Steps
- Learn session basics - StartSession, endSession
- Write simple transaction - Insert and update
- Add error handling - Try-catch blocks
- Implement retry logic - Handle transient errors
- Monitor performance - Measure transaction time
Guarantee consistency with transactions! ✅