Developer Docs

uCart Concurrency & Resilience โ€” Developer Guide

Overview

The uCart (Universal Cart) is the only entity in the platform that aggregates items from multiple independent Malets into a single document. This creates a high write-contention surface: a Visitor browsing three Malets simultaneously can trigger overlapping addToCart, removeFromCart, and recalculateTotals mutations that race against the same MongoDB document.

To prevent lost updates and silent data corruption, the uCart subgraph employs optimistic concurrency control (OCC) with an automatic exponential backoff retry wrapper around all cart mutations.


Problem: The Multi-Malet Race Condition

Unlike a Product (owned by one Malet) or a Murchase (immutable after creation), a Cart document is mutated by:

  1. Multiple Malet contexts โ€” Adding from Malet A and Malet B simultaneously
  2. Recalculation triggers โ€” Every item add/remove triggers total recompute
  3. Real-time sync โ€” Frontend optimistic updates can trigger rapid successive writes

Without protection, a classic read-modify-write race occurs:

Thread A: read cart (v1) โ†’ add item โ†’ write cart (v1 โ†’ v2) โœ…
Thread B: read cart (v1) โ†’ add item โ†’ write cart (v1 โ†’ v2) โŒ LOST UPDATE

Thread B's write silently overwrites Thread A's changes because both read the same version.


Solution: Optimistic Concurrency Control

1. Entity Versioning (`__v`)

The Cart entity enables MongoDB's built-in optimisticConcurrency flag via Typegoose:

@modelOptions({ schemaOptions: { optimisticConcurrency: true } })
export class Cart {
  // MongoDB automatically manages __v (version key)
  // Every save() increments __v and checks it hasn't changed
}

When cart.save() is called, Mongoose checks that __v in the database matches __v in the document. If another process incremented __v between read and write, a VersionError is thrown instead of silently overwriting.

2. `withRetry` Higher-Order Function

All cart mutations are wrapped in a withRetry HOC that catches VersionError and retries with exponential backoff:

private async withRetry<T>(
  operation: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 50,
): Promise<T> {
  let retries = 0;
  while (true) {
    try {
      return await operation();
    } catch (error: any) {
      if (
        (error.name === 'VersionError' ||
          error.message?.includes('VersionError')) &&
        retries < maxRetries
      ) {
        retries++;
        const backoffDelay = baseDelay * Math.pow(2, retries - 1);
        // Jitter prevents thundering herd
        await sleep(backoffDelay + Math.random() * backoffDelay);
        continue;
      }
      throw error;
    }
  }
}

Key details:

  • Max 3 retries with base delay of 50ms โ†’ 50ms, 100ms, 200ms
  • Random jitter prevents multiple clients from retrying in lockstep
  • Only retries VersionError โ€” other errors propagate immediately

3. Read/Write Decoupling

A critical architectural decision: read operations never trigger writes. The getCartById query returns cart data without recalculating totals. Recalculation only happens inside mutation paths (addToCart, removeFromCart, mergeCarts), ensuring that read-heavy operations (browsing, polling) don't compete for write locks.


Protected Operations

All of the following mutations are wrapped in withRetry:

Mutation What It Does Why OCC Matters
addToCart Adds item to cart, recalculates totals Multiple Malets adding simultaneously
removeFromCart Removes item, recalculates Concurrent remove + add from different Malets
updateQuantity Changes item quantity Visitor rapidly clicking +/- buttons
clearCart Empties all items Race between clear and add
mergeCarts Combines anonymous + authenticated carts Login triggers merge while browsing continues

Error Handling

If all retries are exhausted, the original VersionError propagates as a GraphQL error:

{
  "errors": [{
    "message": "Cart was modified by another operation. Please try again.",
    "extensions": { "code": "VERSION_CONFLICT" }
  }]
}

The frontend handles this by re-fetching the cart and replaying the user's action โ€” this is transparent to the Visitor.


Testing

The uCart test suite validates concurrency behavior with 161 passing tests, including:

  • Version increment verification on successful save()
  • VersionError propagation on stale writes
  • Retry exhaustion and error escalation
  • Read operations confirming no side-effect writes

Monitoring

Cart concurrency conflicts are logged at WARN level with full context:

[WARN] CartService: Concurrency conflict (VersionError). 
       Retrying in 52ms (Attempt 1/3)

Persistent VersionError escalations (all retries exhausted) are logged at ERROR and should be monitored โ€” they indicate unusually high write contention that may warrant architectural review (e.g., per-Malet sub-carts).


Future Considerations

Improvement Description Status
Per-Malet Sub-Carts Shard cart into per-Malet sub-documents to reduce contention surface ๐Ÿ“‹ Planned
Redis Write-Through Buffer high-frequency mutations in Redis, flush to Mongo on debounce ๐Ÿ“‹ Planned
Conflict Telemetry Emit retry/failure metrics for platform observability ๐Ÿ“‹ Planned