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:
- Multiple Malet contexts โ Adding from Malet A and Malet B simultaneously
- Recalculation triggers โ Every item add/remove triggers total recompute
- 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 |
Related
- Gateway Rate Limiting โ request throttling that indirectly reduces cart contention
- Debugging & Troubleshooting โ logging patterns used by the withRetry wrapper
- Vertical Intelligence Reindexer โ another service that writes to Malet entities (IntelligenceMetadata)