Concurrency, Transactions, and Consistency in the Data Model

Decide where correctness must be guaranteed

MongoDB single-document writes are atomic. This makes embedding useful when a business operation updates fields that belong together. But multiple documents may be needed when resources are shared or unbounded.

Conditional updates prevent lost inventory

A checkout operation should not read a stock count, decide in application memory that stock is available, and then blindly write a lower count. Two concurrent requests could both observe the same stock.

Use a conditional atomic update:

const result = db.inventory.updateOne(
  {
    productId,
    available: { $gte: requestedQuantity }
  },
  {
    $inc: { available: -requestedQuantity },
    $set: { updatedAt: new Date() }
  }
);

if (result.modifiedCount !== 1) {
  throw new Error("Insufficient inventory or product unavailable");
}

This protects the inventory decrement for that document. It does not, by itself, guarantee that an order document and the inventory document are committed together.

When a transaction is appropriate

If the business invariant requires an order record and inventory reservation to succeed or fail together, a multi-document transaction may be appropriate. Transactions add coordination and operational costs, so use them intentionally.

A production transaction should account for:

  • transient transaction errors and retry rules,
  • idempotency of the overall checkout request,
  • duplicate submissions,
  • external side effects such as payment calls,
  • transaction duration and contention,
  • failure recovery and reconciliation.

Do not call an external payment service inside a transaction and assume that database rollback can undo the payment. External systems require their own idempotency and compensation strategy.

Alternative: reservation workflow

For workflows that cross services or last longer than a short transaction, a state machine can be more appropriate:

  1. Create an order in pending state with an idempotency key.
  2. Reserve inventory with a conditional update.
  3. Request payment using an idempotency key.
  4. Transition the order to confirmed or failed.
  5. Release the reservation when the workflow fails.
  6. Reconcile incomplete states with a background process.

This pattern does not magically remove consistency complexity; it makes the states and recovery behavior explicit.

Optimistic concurrency

For documents that are edited by multiple users, include a version field and require the expected version in the update filter:

db.documents.updateOne(
  { _id: documentId, version: expectedVersion },
  {
    $set: { title: newTitle },
    $inc: { version: 1 }
  }
);

If modifiedCount is zero, the document may have changed since it was read, or it may no longer match. The application should reload and resolve the conflict instead of overwriting another user's changes silently.

Specifications & Requirements
  • Conditional update:An update whose filter encodes the condition that must still be true.
  • Optimistic concurrency:Detect conflicting edits by checking a version or expected value.
  • Idempotency key:A stable request identifier used to prevent duplicate execution of the same logical operation.
Note

Atomicity is scoped to the operation MongoDB guarantees. A single-document atomic update does not make a multi-document business workflow atomic, and a database rollback cannot undo an external payment.

Use a version check to prevent silent overwrites
const result = await db.collection("documents").updateOne(
  { _id: documentId, version: expectedVersion },
  {
    $set: { title: newTitle, updatedAt: new Date() },
    $inc: { version: 1 }
  }
);

if (result.modifiedCount !== 1) {
  throw new Error("Document changed; reload before saving");
}
Key Takeaways
  • Encode critical invariants in update filters where possible.
  • Use transactions when cross-document atomicity is truly required.
  • Design idempotency, retry, and compensation behavior for multi-step workflows.
Knowledge Check

What does a conditional inventory update with `available: { $gte: quantity }` help prevent?