MongoDB Multi-Document Transactions
Guarantee consistency with ACID transactions across multiple operations.
Quick Start
Basic Transaction
javascript
const session = client.startSession()
try {
await session.withTransaction(async () => {
// All operations here are atomic
await users.insertOne({ name: 'John' }, { session })
await accounts.insertOne({ userId: 'xxx', balance: 100 }, { session })
// If any fails, ALL roll back
})
} catch (error) {
console.error('Transaction failed:', error)
} finally {
await session.endSession()
}
Real-World: Money Transfer
javascript
async function transferMoney(fromId, toId, amount) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
// Deduct from account A
await accounts.updateOne(
{ _id: fromId },
{ $inc: { balance: -amount } },
{ session }
)
// Add to account B
await accounts.updateOne(
{ _id: toId },
{ $inc: { balance: amount } },
{ session }
)
// Both succeed, or both rollback - NO PARTIAL TRANSFERS!
})
} catch (error) {
// Transaction failed, money not transferred
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
javascript
const session = client.startSession()
// Configure session
const session = client.startSession({
defaultTransactionOptions: {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' },
readPreference: 'primary'
}
})
Session Lifecycle
javascript
// 1. Start session
const session = client.startSession()
// 2. Start transaction
session.startTransaction()
// 3. Execute operations with session
await collection.insertOne(doc, { session })
// 4. Commit or abort
await session.commitTransaction() // Success
await session.abortTransaction() // Rollback
// 5. End session
await session.endSession()
Transaction Options
Read Concern
javascript
// snapshot: See committed data at transaction start
// local: See latest data (default)
// majority: See data confirmed on majority
// linearizable: Latest confirmed data
await session.withTransaction(async () => {
// Operations
}, {
readConcern: { level: 'snapshot' }
})
Write Concern
javascript
// w: 1 (default): Acknowledged by primary
// w: 'majority': Acknowledged by majority
// w: N: Acknowledged by N replicas
await session.withTransaction(async () => {
// Operations
}, {
writeConcern: { w: 'majority' }
})
Read Preference
javascript
// primary: Read from primary only
// primaryPreferred: Primary, fallback to secondary
// secondary: Read from secondary
// secondaryPreferred: Secondary, fallback to primary
// nearest: Nearest server
{
readPreference: 'primary'
}
Error Handling
Handle Transaction Errors
javascript
async function robustTransaction() {
const session = client.startSession()
try {
await session.withTransaction(async () => {
// Operations
})
} catch (error) {
if (error.hasErrorLabel('TransientTransactionError')) {
// Temporary error - retry
return robustTransaction()
} else if (error.hasErrorLabel('UnknownTransactionCommitResult')) {
// Commit outcome unknown - check state
console.log('Commit outcome unknown')
} else {
// Fatal error
throw error
}
} finally {
await session.endSession()
}
}
Retry Logic
javascript
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 // Success
} catch (error) {
if (error.hasErrorLabel('TransientTransactionError') && attempt < maxRetries) {
continue // Retry
} else {
throw error
}
} finally {
await session.endSession()
}
}
}
Real-World Examples
Order Placement with Inventory
javascript
async function placeOrder(userId, items) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
// 1. Create order
const order = {
userId,
items,
status: 'pending',
createdAt: new Date()
}
const orderResult = await orders.insertOne(order, { session })
// 2. Update inventory
for (const item of items) {
const updated = await products.findOneAndUpdate(
{ _id: item.productId },
{ $inc: { stock: -item.quantity } },
{ session, returnDocument: 'after' }
)
// Check if stock went negative
if (updated.value.stock < 0) {
throw new Error('Insufficient inventory')
}
}
// 3. Deduct from user account
const userUpdate = await users.findOneAndUpdate(
{ _id: userId },
{ $inc: { balance: -calculateTotal(items) } },
{ session, returnDocument: 'after' }
)
if (userUpdate.value.balance < 0) {
throw new Error('Insufficient funds')
}
return orderResult.insertedId
})
} catch (error) {
// ALL operations rolled back if any fails
// Inventory stays same
// User balance unchanged
// Order not created
console.error('Order failed:', error.message)
throw error
} finally {
await session.endSession()
}
}
Account Reconciliation
javascript
async function reconcileAccounts(mainId, secondaryIds) {
const session = client.startSession()
try {
await session.withTransaction(async () => {
// Calculate total balance
const secondary = await accounts.find(
{ _id: { $in: secondaryIds } },
{ session }
).toArray()
const totalBalance = secondary.reduce((sum, acc) => sum + acc.balance, 0)
// Update main account
await accounts.updateOne(
{ _id: mainId },
{ $set: { balance: totalBalance } },
{ session }
)
// Delete secondary accounts
await accounts.deleteMany(
{ _id: { $in: secondaryIds } },
{ session }
)
// Log reconciliation
await reconciliationLog.insertOne({
mainId,
secondaryIds,
totalBalance,
timestamp: new Date()
}, { session })
// All succeed together, or all rollback
})
} finally {
await session.endSession()
}
}
Limitations & Considerations
Transaction Limits
code
- Maximum 16MB of write operations - Cannot create/drop collections - Cannot create/drop indexes - Cannot alter collections - Cannot write to system collections
Performance Impact
code
- 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! ✅