Transaction Processing: ACID and Multi-Document Consistency

Transactions ensure the atomicity of multi-document operations—mastering this concept enables the development of reliable financial and order systems.

1. What You'll Learn


100%
sequenceDiagram
    participant App as Applications
    participant DB as MongoDB<br/>Dungeon Collection
    participant Log as Log

    App->>DB: startTransaction()
    activate DB
    DB-->>App: session

    App->>DB: Deduct $100
    DB-->>App: OK
    App->>DB: Add $100
    DB-->>App: OK
    App->>DB: Create a Transaction Log
    DB-->>App: OK

    alt All successful
        App->>DB: commitTransaction()
        DB-->>App: ✅ Committed
        DB->>Log: Persistence
    else Any failure
        App->>DB: abortTransaction()
        DB-->>App: ❌ Rollback
        Note over DB: All changes have been reversed<br/>Data is rolled back to the state before the transaction
    end

    deactivate DB

2. Why Are Transactions Necessary?

Concept Explanation: A transaction is a logical unit of database operations that ensures all operations within it either succeed entirely or are rolled back entirely. When writing to multiple documents or collections (such as in fund transfers or order placements), data consistency cannot be guaranteed without transactions—partial success and partial failure would result in dirty data.

How It Works: MongoDB 4.0 and later support multi-document ACID transactions, implemented using snapshot isolation based on the WiredTiger engine. When a transaction begins, a snapshot is created, and all read and write operations are performed relative to that snapshot; upon commit, changes are atomically applied to the data files, and upon rollback, all changes are discarded. Under the hood, transactions rely on the replica set’s oplog to ensure persistence and replication.

A Detailed Explanation of the ACID Principles:

Feature Meaning MongoDB Implementation Principle
Atomicity Transactions either succeed entirely or fail entirely commit / abort WiredTiger ensures this through the rollback log: atomic writes during commit, and recovery using the rollback log during rollback
Consistency Data integrity constraints remain unchanged Schema validation + transaction constraints All constraints are checked before a transaction is committed; if any are violated, the transaction is rejected
Isolation Concurrent transactions do not interfere with one another Snapshot Isolation A data snapshot is taken at the start of a transaction; all reads and writes are based on that snapshot throughout the transaction, and the transaction is not affected by other transactions
Durability Permanently stored after transaction commit Journal + replica set oplog Writes are first recorded in the journal (WAL), then replicated to a majority of nodes in the replica set

MVCC Mechanism: MongoDB implements snapshot isolation through Multi-Version Concurrency Control (MVCC). Each document maintains multiple historical versions; transactions read data from the version corresponding to their start timestamp, and write operations create new versions without overwriting the old ones. The new version becomes visible to other transactions only upon commit.

100%
graph TB
    subgraph "MVCC Multiple Versions"
        D1["Document v1<br/>balance: 1000"]
        D2["Document v2<br/>balance: 900<br/>(Transaction A Edit)"]
        D3["Document v3<br/>balance: 1100<br/>(Transaction B Edit)"]
    end

    subgraph "Transaction Snapshot Read"
        T1["TransactionsA (t1)<br/>Read v1"] --> R1["balance: 1000"]
        T2["TransactionsB (t2)<br/>Read v1"] --> R2["balance: 1000"]
    end

    D1 --> D2
    D1 --> D3

    style D2 fill:#cce5ff
    style D3 fill:#d4edda

Use Cases:

JAVASCRIPT
// ❌ Counterexample: Transfer without a transaction
async function transfer(fromUserId, toUserId, amount) {
  await User.updateOne({ _id: fromUserId }, { $inc: { balance: -amount } });
  // System Crash!
  await User.updateOne({ _id: toUserId }, { $inc: { balance: amount } });
  // The user's balance was deducted, but the recipient did not receive the payment
}

3. Basic Usage of Transactions

Concept Explanation: MongoDB transactions are managed through Session objects—startSession() creates a session, startTransaction() starts a transaction, commitTransaction() commits, and abortTransaction() rolls back. All operations within a transaction must be passed as { session } parameters.

How It Works: The complete lifecycle of a transaction is as follows: start session → start transaction → execute operations (with session parameters) → commit/rollback → end session. Upon commit, WiredTiger atomically writes all changes to the journal; upon rollback, it reverts all changes using the rollback log. The default transaction timeout is 60 seconds; transactions automatically roll back if they time out.

Transaction Lifecycle:

100%
stateDiagram-v2
    [*] --> StartSession: startSession()
    StartSession --> Active: startTransaction()
    Active --> Active: Perform an action (with session)
    Active --> Committed: commitTransaction()
    Active --> Aborted: abortTransaction()
    Committed --> [*]: endSession()
    Aborted --> [*]: endSession()

    note right of Active: Default 60s automatic rollback on timeout
    note right of Committed: Persist changes to journal

Grammar Rules:

Step Method Description
1 startSession() Create a session
2 startTransaction() Start Transaction
3 Operation + {session} All read and write operations must pass the session
4a commitTransaction() All successful → Submit
4b abortTransaction() Any failure → Rollback
5 endSession() Release session resources
JAVASCRIPT
// === MongoDB 4.0+ Multi-document transactions ===
const session = db.getMongo().startSession();

session.startTransaction();
try {
  // 1. Deduct
  db.users.updateOne(
    { _id: fromUserId },
    { $inc: { balance: -amount } },
    { session }
  );

  // 2. Add
  db.users.updateOne(
    { _id: toUserId },
    { $inc: { balance: amount } },
    { session }
  );

  // 3. Commit Transaction
  await session.commitTransaction();
} catch (err) {
  // 4. Rollback
  await session.abortTransaction();
  throw err;
} finally {
  session.endSession();
}

Key Points Analysis:

  1. The operation to forget the transmission of { session } is not part of a transaction and is not protected by the transaction.
  2. commitTransaction and abortTransaction are idempotent operations; calling them repeatedly will not result in an error.
  3. Transactions are automatically rolled back upon timeout; the application layer should set a reasonable timeout and implement retry logic.

4. ACID Properties

Concept Overview: ACID refers to the four core guarantees of database transactions—Atomicity, Consistency, Isolation, and Durability. Understanding how ACID is implemented in MongoDB is the foundation for designing a reliable transactional system.

Detailed Explanation of Isolation Levels: MongoDB supports three read isolation levels, which are controlled via readConcern:

Isolation Level readConcern Behavior Use Case
Read unsubmitted local Read latest local data (may roll back) Default, performance-first
Read Committed majority Read data that has been confirmed by the majority Strong consistency requirement
Snapshot Isolation snapshot In-Transaction Read-Consistent Snapshot In-Transaction Default
100%
sequenceDiagram
    participant T1 as Transactions1
    participant T2 as Transactions2
    participant DB as MongoDB

    Note over DB: Initial balance=1000

    T1->>DB: startTransaction(readConcern: snapshot)
    T1->>DB: Read balance → 1000

    T2->>DB: startTransaction()
    T2->>DB: balance -100 → Write 900
    T2->>DB: commitTransaction()

    T1->>DB: Read balance → 1000 (Snapshot isolation, can't see T2 changes)

    Note over T1: Snapshots ensure intra-transaction consistency

    T1->>DB: commitTransaction()
    Note over DB: Conflict detection → If T1 also modifies balance, an error will be reported
Feature Meaning MongoDB Implementation
Atomicity The transaction either succeeds entirely or fails entirely commit / abort
Consistency Data Integrity Constraints Schema Validation + Transactions
Isolation Concurrent transactions do not interfere with one another Snapshot isolation
Durability Permanently stored after transaction commit Journal + replica set

5. readConcern / writeConcern / readPreference

Concept Explanation: These three settings form the "trinity" of MongoDB's transaction consistency control, governing read consistency, write durability, and read routing policies, respectively. They determine the trade-off between consistency and performance for transactions.

How It Works:

(1) Write Concern

Principle: After a write operation is written to the Primary, it must wait for a specified number of confirmations from the Secondary that the data has been replicated before returning a success.

Parameter Value Behavior Consistency Performance
w 1 Primary confirmation only Low Fastest
w majority confirmed by the majority of nodes high slow
j true Write to disk journal Fastest Slowest
wtimeout ms Wait timeout Timeout error
JAVASCRIPT
session.startTransaction({
  writeConcern: {
    w: 'majority',         // Confirmed by a majority of nodes
    j: true,               // Write to disk journal
    wtimeout: 5000         // 5 Timeout in seconds
  }
});

(2) Read Concern

Principle: Controls which versions of data a read operation can access—whether the latest local version (which may not have been committed) or the version with the most confirmations (which has been committed).

Level Description Applicable Scenarios
local Read the latest local data (default) Performance-oriented; allows reading uncommitted data
majority Read data that has been mostly confirmed Strong consistency
snapshot Snapshot isolation (within a transaction only) Default for transactions; prevents phantom reads
JAVASCRIPT
session.startTransaction({
  readConcern: {
    level: 'majority'      // Read Submitted Data
  },
  writeConcern: { w: 'majority' }
});

(3) Read Preferences

Principle: Controls whether read operations are routed to the Primary or Secondary node, thereby implementing read-write separation.

Pattern Behavior Applicable Scenarios
primary Read-only primary node Strongly consistent transactions
primaryPreferred Primary first; if unavailable, fallback to secondary General scenarios
secondary Read-only secondary node Reporting/analysis, offloading the primary node
secondaryPreferred Read-first, fallback to primary if unavailable Read-heavy, write-light
nearest Lowest Network Latency Geographically Distributed Cluster
JAVASCRIPT
session.startTransaction({
  readPreference: 'primary'             // Read-Only Primary Node
});

session.startTransaction({
  readPreference: 'secondary'           // Read from a child node
});

session.startTransaction({
  readPreference: 'secondaryPreferred'  // Priority Node
});

Recommended Three-Piece Sets:

Scenario writeConcern readConcern readPreference
Financial transactions majority + j:true snapshot primary
General transactions majority majority primary
Report Analysis local secondary
Development and Testing w:1 local primaryPreferred

6. Mongoose Transaction Encapsulation

Concept Explanation: Mongoose provides a more elegant transaction API—the startSession() and session parameters. However, the try-catch-commit-abort boilerplate code used for manually managing transactions is verbose; encapsulating it into the withTransaction utility function can significantly simplify business logic code.

How It Works: Mongoose’s withTransaction wrapper automates session lifecycle management (start → commit/abort → end), allowing business functions to focus solely on core logic. Mongoose also supports passing { session } parameters to Model operations (findById, create, updateOne), enabling seamless integration of CRUD operations within transactions.

Comparison of Transaction Encapsulation Patterns:

Pattern Code Size Error Handling Retry Support Use Cases
Manual try-catch Multiple Manual None Simple scenarios
withTransaction Encapsulation Few Automatic Can be added Recommended for production
mongoose.connection.transaction Minimum Automatic Built-in Mongoose 6+
JAVASCRIPT
// === mongoose Transaction Encapsulation ===
async function withTransaction(callback) {
  const session = await mongoose.startSession();
  session.startTransaction();
  try {
    const result = await callback(session);
    await session.commitTransaction();
    return result;
  } catch (err) {
    await session.abortTransaction();
    throw err;
  } finally {
    session.endSession();
  }
}

// === Usage: Transfer ===
async function transfer(fromUserId, toUserId, amount) {
  return withTransaction(async (session) => {
    const fromUser = await User.findById(fromUserId).session(session);
    if (fromUser.balance < amount) {
      throw new Error('Insufficient balance');
    }

    await User.updateOne(
      { _id: fromUserId },
      { $inc: { balance: -amount } },
      { session }
    );

    await User.updateOne(
      { _id: toUserId },
      { $inc: { balance: amount } },
      { session }
    );

    await TransactionLog.create([{
      fromUserId,
      toUserId,
      amount,
      createdAt: new Date()
    }], { session });

    return { success: true };
  });
}

▶ Example 2: Mongoose Transaction Retry Encapsulation

JAVASCRIPT
// Alice's ShopHub Financial System: Transaction encountered WriteConflict automatic retry
async function withRetryTransaction(callback, maxRetries = 3) {
  let lastError;
  for (let i = 0; i < maxRetries; i++) {
    const session = await mongoose.startSession();
    session.startTransaction({
      readConcern: { level: 'snapshot' },
      writeConcern: { w: 'majority' }
    });
    try {
      const result = await callback(session);
      await session.commitTransaction();
      return result;
    } catch (err) {
      await session.abortTransaction();
      lastError = err;
      if (err.errorLabels && err.errorLabels.includes('TransientTransactionError')) {
        console.log(`Retry ${i + 1}/${maxRetries} due to WriteConflict`);
        continue;
      }
      throw err;
    } finally {
      session.endSession();
    }
  }
  throw lastError;
}

// Usage
await withRetryTransaction(async (session) => {
  await User.updateOne({ _id: fromId }, { $inc: { balance: -100 } }, { session });
  await User.updateOne({ _id: toId }, { $inc: { balance: 100 } }, { session });
});

7. Transaction Limits

Concept Explanation: MongoDB transactions have clear usage boundaries—they must be performed within a replica set, are subject to size limits, and do not support certain operations. Understanding these limitations is key to avoiding production incidents.

Detailed Explanation of Restrictions:

Constraint Description Reason Mitigation Strategy
Replica Sets Are Required Standalone MongoDB Does Not Support Transactions Transactions Rely on the oplog for Persistence A Single-Node Replica Set Is Permitted in Development Environments
16MB document Total of all operations within a transaction WiredTiger single-document limit Splitting large transactions into smaller ones
Default 60-second timeout maxTransactionLockRequestTimeoutMillis Prevent long transactions from holding locks Adjust the timeout parameter
Cannot perform operations on a capped collection Partial restrictions Rollbacks are not supported for capped collections Avoid operating on capped collections within transactions
Cannot create sets within a transaction Partial restriction (relaxed in 4.4+) Conflict between DDL and transactions Create sets before the transaction
Writing Conflicts Concurrent Modifications to the Same Document Optimistic Locking Mechanism Automatic Retry for TransientTransactionError
Lock wait Long transactions block other operations Intent-to-write lock Shorten transactions to avoid time-consuming operations

Impact on Transaction Performance:

Operation Non-transactional Within a transaction Reason for overhead
Single-document write Benchmark +30–50% Snapshot maintenance + lock management
Multi-document write N independent I/O operations 1 commit Merging transactions into a single I/O operation may actually be faster
Read Baseline +10–20% Additional overhead for snapshot reads
Submit 5–50 ms journal fsync + oplog write

Transaction Best Practices:

Practice Description
Keep transactions as short as possible Avoid long transactions that hold locks; keep them under 100 ms
Avoid computations within transactions Perform complex computations outside transactions; limit transactions to read and write operations only
Retrying WriteConflicts MongoDB 4.0+ Provides errorLabels to Identify Retryable Errors
Prioritize single-document atomic operations updateOne + $inc is atomic in itself; no transaction is required

8. Causal Consistency

Concept Explanation: Causal consistency is a lighter-weight consistency model than strong consistency—it does not guarantee that all operations are globally ordered, but it does guarantee that operations with causal relationships are executed in the correct order. For example, “read the balance first, then deduct the amount”—the deduction operation must be based on the most recently read balance; this is a causal dependency.

How It Works: MongoDB achieves causal consistency through operationTime and clusterTime. Each operation within a session carries the logicalTime of the previous operation, and the server ensures that subsequent operations see the results of preceding operations. Enabling causal consistency requires readConcern: majority + writeConcern: majority.

Causal Consistency vs. Other Consistency Models:

Model Warranty Performance Applicability
Strong consistency (linearizable) Globally ordered Slowest Financial core
Causal Consistency Causal Order Relatively Fast Multi-step Operations
Eventual consistency Unordered Fastest Logs, notifications
Read My Posts My Posts Are Visible Fast User Experience
100%
sequenceDiagram
    participant A as Alice
    participant P as Primary
    participant S as Secondary

    A->>P: Read Balance (readConcern: majority)
    P-->>A: balance=1000, clusterTime=t1

    A->>P: Deduct $100 (writeConcern: majority)
    Note over A,P: Carry afterClusterTime=t1
    P->>S: Copy oplog
    S-->>P: Confirm
    P-->>A: OK, clusterTime=t2

    A->>P: View Transaction History (readConcern: majority)
    Note over A,P: Carry afterClusterTime=t2
    P-->>A: Includes records of payments that have just been deducted ✅

    Note over A,P: Consistency of Cause and Effect:If you read it, you're sure to recognize your own previous writing.
JAVASCRIPT
// === Causal Consistency:Ensure the Correct Order of Operations ===
const session = db.getMongo().startSession();
session.startTransaction({
  readConcern: { level: 'majority' },
  writeConcern: { w: 'majority' }
});

// Operation 1:Read the current balance
const account = db.accounts.findOne({ userId: 'user_001' }, { session });
// Operation 2:Write Based on Read Results
db.accounts.updateOne(
  { userId: 'user_001' },
  { $set: { balance: account.balance - 100 } },
  { session }
);
// Guarantee:Operation 2 What you see is the operation 1 Subsequent Status

Key Points Analysis:

  1. To ensure causal consistency, you must use a session, and both readConcern and writeConcern must be set to majority.
  2. When reading across nodes (readPreference: secondary), causal consistency ensures that the read reflects the write made by the local node.
  3. Causal consistency is the underlying mechanism for MongoDB's multi-document transactions and Change Streams.

▶ Example: A Complete Hands-On Guide to E-commerce Order Transactions

JAVASCRIPT
// Scene: Order placement process (Order + Inventory Deduction + Wallet Deduction + Logging), Fully Atomic
// Introduction: A replica set is required. Transactions must be in progress

// Initialize Data
db.products.insertOne({ sku: 'PHONE-001', stock: 10, price: 599 });
db.users.insertOne({ _id: 'user_001', balance: 1000 });
db.transaction_logs.createIndex({ userId: 1, createdAt: -1 });

// Complete Transaction Functions
async function placeOrder(userId, items) {
  const session = db.getMongo().startSession();
  session.startTransaction({
    readConcern: { level: 'snapshot' },
    writeConcern: { w: 'majority' }
  });

  try {
    // 1. Calculate the total amount + Check Inventory (Atomic Read)
    let total = 0;
    for (const item of items) {
      const product = db.products.findOne(
        { sku: item.sku, stock: { $gte: item.qty } },
        { session }
      );
      if (!product) {
        throw new Error(`Out of Stock: ${item.sku}`);
      }
      total += product.price * item.qty;
    }

    // 2. Check User Balance
    const user = db.users.findOne({ _id: userId }, { session });
    if (user.balance < total) {
      throw new Error('Insufficient balance');
    }

    // 3. Inventory Deduction (Conditional, Preventing Overselling)
    for (const item of items) {
      const result = db.products.updateOne(
        { sku: item.sku, stock: { $gte: item.qty } },
        { $inc: { stock: -item.qty } },
        { session }
      );
      if (result.modifiedCount === 0) {
        throw new Error(`Inventory deduction failed: ${item.sku}`);
      }
    }

    // 4. Deduct from the user's balance
    db.users.updateOne(
      { _id: userId, balance: { $gte: total } },
      { $inc: { balance: -total } },
      { session }
    );

    // 5. Create an Order
    const orderResult = db.orders.insertOne({
      userId,
      items,
      total,
      status: 'paid',
      createdAt: new Date()
    }, { session });

    // 6. Record the transaction log
    db.transaction_logs.insertOne({
      userId,
      orderId: orderResult.insertedId,
      amount: total,
      type: 'purchase',
      createdAt: new Date()
    }, { session });

    // 7. Commit Transaction
    session.commitTransaction();
    return { success: true, orderId: orderResult.insertedId };

  } catch (err) {
    // Any failure → Roll Back All
    session.abortTransaction();
    return { success: false, error: err.message };
  } finally {
    session.endSession();
  }
}

// Execute: Place an Order
placeOrder('user_001', [
  { sku: 'PHONE-001', qty: 1 }
]);

// Testing Rollback Scenarios: Deliberately Creating Errors
placeOrder('user_001', [
  { sku: 'NONEXIST', qty: 1 }  // The product does not exist.
]);
// Throw an exception → Transaction Rollback → Inventory, balance, order, all logs remain unchanged

// Verifying Atomicity:
// db.products.findOne({ sku: 'PHONE-001' }) → stock: 10 (Not deducted)
// db.users.findOne({ _id: 'user_001' }) → balance: 1000 (Not deducted)

Output: When a transaction succeeds, all changes are committed together; when a transaction fails, all changes are rolled back, ensuring data consistency.

❓ FAQ

Q Can a transaction guarantee strong data consistency?
A With a replica set + writeConcern majority + readConcern majority + readPreference primary, yes.
Q How much worse is transaction performance?
A It’s 30–50% slower than non-transactional operations. Transactions involve locks and snapshots.
Q Can a single-node MongoDB instance use transactions?
A No. It must be a replica set or a sharded cluster.

📖 Summary


📝 Exercises

  1. Basic Question (⭐): Implement a transfer transaction using mongosh (including try-catch rollback).
  2. Basic Problem (⭐): Use Mongoose to wrap the withTransaction utility function.
  3. Advanced Exercise (⭐⭐): Implement an order transaction (place order + deduct inventory + create order + clear shopping cart—all atomic).
  4. Advanced Exercise (⭐⭐): Test transaction rollback in case of failure (intentionally throw an error to verify atomicity).
  5. Challenge (⭐⭐⭐): Build a complete e-commerce transaction system (orders + inventory + wallet + logs) that supports distributed rollback.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏