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
  }
}

🧠 Test Your Knowledge

What happens when you call abortTransaction()?