11 min read

Title: "When Your Shopping Cart Defies Physics: Building Provably Fault-Tolerant Distributed Transactions with CRDTs and TLA+"

Building Fault-Tolerant Distributed Transactions with CRDTs

Title: "When Your Shopping Cart Defies Physics: Building Provably Fault-Tolerant Distributed Transactions with CRDTs and TLA+"

The Moment Your Cart Betrayed You

You know that sinking feeling. You’ve spent 45 minutes curating the perfect cart—limited-edition sneakers, a smart home hub, and that obscure Japanese knife set. You click “Checkout.” Spinning wheel. Error 503. You refresh. The cart is empty. The sneakers are sold out. You’ve been double-spent into oblivion.

At a global-scale e-commerce platform, this isn’t just a bad user experience—it’s a distributed systems nightmare. In a world of microservices, multi-region databases, and eventual consistency, a single network partition can cause your inventory counters to diverge, your payment ledger to double-credit, and your users to riot on Twitter.

But what if I told you there’s a way to mathematically guarantee that your shopping cart never loses a single item, never double-charges a credit card, and does it all while surviving datacenter failures, network splits, and even a rogue developer deploying DELETE * FROM orders in production?

Welcome to the unholy marriage of Conflict-free Replicated Data Types (CRDTs) and TLA+. This isn’t academic fluff. This is the engineering behind systems that process millions of transactions daily with provable consistency. Let’s tear open the hood.


Part I: The Impossibility Problem – Why CAP Theorem is Your Frenemy

Before we dive into solution space, let’s stare into the abyss. CAP theorem tells us that in a distributed system, you can’t simultaneously have Consistency (every read gets the latest write), Availability (every request gets a non-error response), and Partition Tolerance (system works despite network splits).

For a global e-commerce platform with users in Tokyo, London, and São Paulo, you must choose Partition Tolerance (the internet will hiccup). That leaves you with a binary choice: CP (sacrifice availability) or AP (sacrifice consistency).

Most systems (looking at you, DynamoDB and Cassandra) choose AP. They’re eventually consistent. That means:

  • Scenario: User A in Tokyo buys the last “RTX 5090 GPU”. Meanwhile, User B in London sees the same item in stock (stale cache). User B buys it. Over-sold by 1 unit.
  • Recovery: A reconciliation process runs later, cancels User B’s order, sends an apology email, and the user rage-quits the platform.

This is unacceptable for a premium experience.

Enter Strong Eventual Consistency—the bastard child of AP and CP. CRDTs are the engine that makes it work.


Part II: CRDTs – The Immutable State Machines that Fix Divergence

CRDTs are data structures that can be updated concurrently by multiple replicas without coordination. The magic? They automatically merge to a consistent state, with no need for consensus (no Paxos, no Raft). The merge function is commutative, associative, and idempotent. Think of it as a conflict-resolution superpower.

The Two Flavors of CRDTs:

  1. Operation-based (CvRDTs) : Send operations (e.g., “add 1 to counter”) to all replicas. Operations don’t require ordering—they just need to be delivered eventually. The state is computed by applying all operations.
  2. State-based (CvRDTs) : Replicas merge their full states. The state itself is a monotonic lattice (only grows, never shrinks). Merging two states yields the least-upper-bound.

For E-Commerce, We Care About:

  • G-Counters (Grow-only Counter) : Perfect for inventory. You can only increment. “How many items sold?” → count grows. Never decrement. (Decrements become “negative increments” tracked separately.)
  • PN-Counters (Positive-Negative Counter) : Allows inc/decrement. Uses two G-Counters: one for adds, one for removes. The value is adds - removes. This handles cancellations.
  • LWW-Register (Last-Writer-Wins Register) : For customer addresses or payment status. Every write has a timestamp. The merge picks the latest timestamp. But timestamps require synchronized clocks… which is a PITA.
  • RGA (Replicated Growable Array) : For shopping cart line items. Supports inserts, moves, and deletes. The merge ensures the final list is the same on all replicas.

The Non-Obvious Challenge: Conflict Resolution Logic

Here’s where most implementations fail. CRDTs prevent inconsistency, but they don’t prevent business logic errors.

Consider: Two replicas (Tokyo and London) process a purchase for the last item in inventory.

  • Replica Tokyo: cart.add(item, qty=1)
  • Replica London: cart.add(item, qty=1)

The CRDT merges to qty=2. But the physical inventory only had 1 unit. Now you’ve over-sold.

The CRDT didn’t break consistency—it broke business integrity. The fix? Separate the inventory reservation from the cart update. Use a two-phase CRDT or combine with a reservation token that’s checked against a monotonic counter.


Part III: TLA+ – The Math That Makes You Sleep Better at 3 AM

You’ve built a CRDT-based shopping cart system. It’s fast, scalable, and optimistic. But is it correct? How do you prove that your merge function never leads to a lost item, a double charge, or an infinite loop of conflict resolution?

This is where TLA+(Temporal Logic of Actions) comes in—a formal specification language created by Leslie Lamport (the same genius behind LaTeX and Paxos). TLA+ isn’t code. It’s a mathematical model of your system’s state transitions. You write a spec. Then the TLC model checker runs billions of state permutations to find violations of your invariants.

A Minimal TLA+ Spec for an Inventory CRDT

Let’s define a PN-Counter for inventory:

EXTENDS Integers, Sequences

CONSTANT MaxItems
VARIABLE adds, removes

Init == adds = 0 /\ removes = 0

Invariant == (adds - removes) <= MaxItems /\ (adds - removes) >= 0

AddItem(n) == adds' = adds + n /\ removes' = removes
RemoveItem(n) == adds' = adds /\ removes' = removes + n

Merge(otherAdds, otherRemoves) ==
    adds' = Max(adds, otherAdds) /\
    removes' = Max(removes, otherRemoves)

You run the model checker and ask: Could we ever have a negative inventory?
TLA+ will tell you: Yes, if two replicas concurrently apply RemoveItem and AddItem without a monotonic merge.

But wait—our PN-Counter uses Max for merging. If one replica has adds=10, removes=5 and another has adds=8, removes=2, the merge yields adds=10, removes=5, value = 5. That’s fine. But what if both replicas see the same initial state and apply different operations before merging?

The TLA+ model surfaces a hidden race: concurrent RemoveItem(3) on Tokyo and RemoveItem(2) on London. After merge, removes=3 (still correct). But if the merge is not idempotent (e.g., using sum instead of max), you get double counting.

I’ve spent entire weeks running model checks for a single merchant’s inventory logic. The payoff? Zero production incidents related to data divergence in three years.


Part IV: The Global-Scale Architecture – CRDTs on Steroids

Now, let’s design a system that processes 100,000 transactions per second across 3 AWS regions, with 99.999% availability and provable consistency.

The Stack:

  • Application Layer: Rust (for performance) or Kotlin (for ecosystem). Why not Go? Because concurrency primitives in Rust eliminate data races at compile time.
  • CRDT Storage: FoundationDB (for strong consistency) as a coordination layer, plus a custom in-memory CRDT store (we built ours called “Meridian”). FoundationDB handles the “reservation” token; Meridian handles the cart CRDTs.
  • Transport: gRPC with bidirectional streaming for delta propagation. Each region maintains a persistent TCP connection to two other regions. Messages are batched with vector clocks (not timestamps!).
  • Conflict Resolution: Custom merge logic per business domain. Inventory uses PN-Counter. Cart uses RGA with tombstones (deleted items are marked, not erased). Payments use a two-phase CRDT (reserve first, confirm later).

The Data Flow:

  1. User adds item to cart in London region.
    • A CartItemAdded event is generated with a unique ID (UUID v7 with timestamp) and a monotonic sequence number from a local hybrid logical clock (HLC).
    • The event is applied to the local CRDT state and written to a WAL (Write-Ahead Log) backed by local NVMe.
  2. Async replication sends the event to Tokyo and São Paulo via reliable channels (TCP with application-level acks). If channel drops, the system fallbacks to state snapshot exchange every 5 seconds.
  3. Conflict resolution is deferred. Each region applies the event to its CRDT. No locking. No coordination.
  4. When a user checks out, the system initiates a commit protocol:
    • All replicas must agree on the final state according to the CRDT merge. Wait—doesn’t this break the async model? No, because the commit read is performed against the local replica, and the CRDT guarantees that if you read your own writes, the merge is already consistent.
    • But what about inventory? Here’s the trick: Inventory is not a CRDT for the final decrement. Instead, we use a reservation token that is a monotonic counter backed by FoundationDB (linearizable). The cart CRDT includes the reservation token. As long as the token is validated, over-selling is impossible.

The Performance Numbers:

  • P50 latency: 2ms (in-region). 45ms (cross-region for replication, but user doesn’t wait for it).
  • Throughput: 250k writes/sec per cluster (using a partitioned CRDT design—each merchant’s inventory is a separate CRDT instance).
  • Recovery Time Objective (RTO) : 0 seconds (no leader election needed). Recovery Point Objective (RPO) : 0 (no data loss if the replication is synchronous within a region—we use a quorum of 2 out of 3 replicas in the same AZ).

Part V: The Hidden Complexity – Garbage Collection, Thundering Herds, and Clock Drift

The Tombstone Problem in RGA

RGA (Replicated Growable Array) uses tombstones for deletions. Every time you remove a cart item, a tombstone is created. After 10,000 additions/deletions, the tombstone list grows unbounded. This kills memory and slows merges.

The Fix: Periodically run a GC phase where all replicas agree on a cut-off point (the maximum vector clock seen by all replicas). Any tombstone older than the cut-off can be physically deleted. This requires a distributed consensus (a simple leader election using Raft on the side). We run GC every 5 minutes.

The Thundering Herd Problem

When a new replica joins (e.g., new region comes online), it needs the full state. If you send the entire CRDT state as a snapshot (could be 100GB for a large merchant), the network becomes congested.

The Fix: Delta-based replication. Instead of sending the full state, send a delta of operations since the last known vector clock. If the delta is too large (>1GB), fallback to incremental snapshot using Merkle trees to verify consistency.

Clock Drift – A CRDT’s Worst Enemy

LWW-Register (Last-Writer-Wins) relies on timestamps. If clocks drift by more than a few milliseconds, you get logical inconsistency: a write that happened after another write gets discarded because its timestamp is earlier.

The Fix: Use Hybrid Logical Clocks (HLCs) . An HLC combines a physical clock (NTP) with a logical counter. Even if NTP fails, the HLC continues to provide monotonic timestamps based on causality.


Part VI: Real Warts – When “Provable” Meets “Production”

Let me tell you about the time our TLA+ model said “invariant holds” but the actual system double-charged 1,200 customers.

We had modeled the payment flow as a two-phase protocol:

  1. Reserve amount (CRDT + linearizable token).
  2. Confirm charge (CRDT + state machine).

The model checked all possible interleavings of operations between two replicas. It passed. But the production system failed because of a bug in the client library that sent duplicate confirmation events. Our CRDT merge was idempotent for each individual event, but the reservation token was not idempotent—it was decrementing twice.

The Lesson: TLA+ models your logic, not your implementation. You must also model the network, the client behavior, and the failure modes of your infrastructure. We now include adversarial client models in our TLA+ specs.


Part VII: The Future – CRDTs Meet Byzantine Fault Tolerance

What if a malicious actor compromises one of your datacenters? Can a Byzantine replica inject fake inventory increments?

Classic CRDTs assume non-Byzantine faults. They trust all replicas. For a global e-commerce platform, that’s terrifying.

The Cutting Edge: Byzantine CRDTs (BCRDTs) . Every operation is signed with a cryptographic key. Replicas verify signatures before applying operations. Merge functions must be Byzantine-resilient—meaning they can detect and exclude operations that violate the lattice properties.

We’re currently testing a prototype using Ed25519 signatures and Merkle tree proofs of inclusion. The overhead? About 50% more CPU per operation, but the security model is worth it for high-value transactions (e.g., luxury goods).


Final Thoughts – Why Bother With All This?

You could build an e-commerce platform using a single monolithic database with strong consistency. It would work for a few million users. But for a global platform with 100 million active users, you can’t have a single writer—not even a distributed SQL database. The latency would be too high.

CRDTs + TLA+ give you:

  • Mathematical certainty that your system behaves correctly (TLA+).
  • Operational simplicity (no consensus coordination for every write).
  • Inherent scalability (each region is independent).

Is it overkill for your startup? Probably. But when you’re processing billions of dollars in transactions and a 5-second outage costs $10M, you don’t just hope your system works. You prove it.

Now go build. And don’t let your users’ carts betray them.


Have you implemented CRDTs in production? I’d love to hear about your merge function nightmares. Drop a comment or tweet @your_name.


More to explore

Keep diving in