Transactions
Execute multiple operations atomically—either all succeed or all fail.
Basic Usage
typescript
await Users.transaction(async (session) => {
// All operations must include { session }
const user = await Users.insertOne(
{ name: 'Alice', email: 'alice@example.com' },
{ session }
);
await Posts.insertOne(
{ title: 'First Post', authorId: user._id },
{ session }
);
// If any operation fails, all are rolled back
});Requirements
Replica Set Required
MongoDB must be running as a replica set for transactions to work. This includes:
- MongoDB Atlas (all clusters)
- Local MongoDB with
--replSetoption - Docker with replica set configuration
Single-node deployments without replica set configuration do not support transactions.
Shared Client Requirement
All collections in a transaction must share the same MongoDB client:
typescript
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const Users = collection({
name: 'users',
schema: { /* ... */ },
client,
database: 'myapp',
});
const Posts = collection({
name: 'posts',
schema: { /* ... */ },
client, // Same client
database: 'myapp',
});
// Now transactions work across both collections
await Users.transaction(async (session) => {
await Users.insertOne({ ... }, { session });
await Posts.insertOne({ ... }, { session });
});Return Values
Transactions can return values:
typescript
const result = await Users.transaction(async (session) => {
const user = await Users.insertOne(
{ name: 'Alice', email: 'alice@example.com' },
{ session }
);
const post = await Posts.insertOne(
{ title: 'First Post', authorId: user._id },
{ session }
);
return { user, post }; // Return created documents
});
console.log(result.user._id);
console.log(result.post._id);Error Handling
If any operation throws, the entire transaction is rolled back:
typescript
try {
await Users.transaction(async (session) => {
await Users.insertOne(
{ name: 'Alice', email: 'alice@example.com' },
{ session }
);
// This will fail and roll back the user insert
throw new Error('Something went wrong');
});
} catch (error) {
console.log('Transaction failed:', error.message);
// The user was NOT inserted
}Common Patterns
Transfer Money
typescript
async function transferMoney(
fromId: string,
toId: string,
amount: number
) {
await Accounts.transaction(async (session) => {
// Debit source account
const from = await Accounts.updateOne(
{ _id: fromId, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);
if (!from) {
throw new Error('Insufficient funds');
}
// Credit destination account
await Accounts.updateOne(
{ _id: toId },
{ $inc: { balance: amount } },
{ session }
);
// Log the transfer
await Transfers.insertOne(
{
from: fromId,
to: toId,
amount,
timestamp: new Date(),
},
{ session }
);
});
}Create User with Related Data
typescript
async function createUserWithProfile(data: UserInput) {
return await Users.transaction(async (session) => {
const user = await Users.insertOne(
{ name: data.name, email: data.email },
{ session }
);
await Profiles.insertOne(
{
userId: user._id,
bio: data.bio,
avatar: data.avatar,
},
{ session }
);
await Settings.insertOne(
{
userId: user._id,
notifications: true,
theme: 'light',
},
{ session }
);
return user;
});
}Atomic Counter with History
typescript
async function incrementCounter(name: string) {
return await Counters.transaction(async (session) => {
const counter = await Counters.updateOne(
{ name },
{ $inc: { value: 1 } },
{ session, upsert: true }
);
await CounterHistory.insertOne(
{
counterName: name,
newValue: counter?.value ?? 1,
timestamp: new Date(),
},
{ session }
);
return counter?.value ?? 1;
});
}Best Practices
Keep transactions short - Long transactions hold locks and can cause contention
Only use when necessary - Transactions have overhead. Use them only when atomicity is required
Handle retries - Transient errors can occur. Consider retry logic for production:
typescript
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (!isTransientError(error)) throw error;
await sleep(Math.pow(2, i) * 100); // Exponential backoff
}
}
throw lastError!;
}
// Usage
await withRetry(() =>
Users.transaction(async (session) => {
// ... operations
})
);- Avoid transactions for single operations - A single
insertOneorupdateOneis already atomic
Limitations
- Maximum transaction runtime: 60 seconds (default)
- Transactions can span multiple collections but must use the same client
- Some operations don't support transactions (e.g., creating indexes)
- Transactions increase memory usage on the server