Why architecture matters here
The CAP theorem forces a choice during partitions: refuse writes (consistency) or accept them and reconcile (availability). Most systems that choose availability reconcile with last-writer-wins — which is not reconciliation but data loss with a timestamp alibi: two users add different items to a shopping cart concurrently, and LWW discards one cart wholesale. CRDTs matter because they make the merge semantic: the add-wins OR-Set keeps both items, the PN-counter sums both increments, the sequence CRDT interleaves both edits at stable positions. The application's intent survives concurrency.
Architecturally, CRDTs relocate coordination from write time to merge time — and that changes system shape profoundly. Writes become local: no quorum round-trip, no leader, no lock; latency is memory-speed, and offline operation is free because a disconnected replica is just a very partitioned one. Replication topology becomes flexible: peer-to-peer gossip, hub-and-spoke through a server, or client-to-client over WebRTC all work, because merges tolerate any delivery order and duplication (idempotence absorbs retries). This is why CRDTs won collaborative editing (Figma-style multiplayer, Notion-like docs via Yjs/Automerge), multi-master geo-replication (Redis Enterprise active-active), and edge/IoT state sync.
The price is equally structural, and you must architect for it. CRDTs guarantee convergence, not invariants: a PN-counter can go negative under concurrent decrements, an inventory 'reserve last item' cannot be expressed without coordination — anything requiring a global precondition still needs consensus somewhere. And convergence bookkeeping (version vectors, tombstones, per-element dots) is real memory and bandwidth that grows with replica count and churn unless actively compacted. Knowing which state fits the CRDT mold and which needs a serialized path is the core design skill.
The architecture: every piece explained
Top row: the convergence core. Replicas A and B accept writes locally and immediately — the defining move. The merge function is where correctness lives: for state-based CRDTs it is the join of a semilattice. A G-Counter is a map replica→count whose join is element-wise max; a PN-Counter is two G-Counters (increments minus decrements); an OR-Set tags each insertion with a unique dot (replica id, counter) so 'add' and 'remove' of the same element commute — remove deletes only the dots it has observed, which is why a concurrent re-add survives (add-wins). Causal metadata — version vectors and dotted version vectors — lets replicas compare states (did you see my updates?), detect concurrency (neither dominates), and keep multi-value registers honest by exposing conflicting values instead of guessing.
Middle row: the three propagation disciplines. State-based replicas periodically ship their whole state and merge on receipt — trivially robust (any order, any duplication, any loss eventually repaired) but bandwidth-heavy for big states. Op-based replicas broadcast each operation exactly once, in causal order, to all peers — tiny messages, but now the transport owes you reliable causal broadcast, and a dropped op is divergence forever. Delta CRDTs are the modern default: mutations produce small delta-states (join-able fragments), which are batched and gossiped with state-based safety at op-based cost. Anti-entropy is the repair loop under all three: periodic gossip rounds where peers exchange digests (version vectors, Merkle trees in Riak's lineage) and send only what the other is missing.
Bottom rows: the applied layer. The type zoo maps intent to algebra: counters for tallies, OR-Sets/OR-Maps for collections, LWW-registers where losing concurrent writes is genuinely acceptable, MV-registers where it is not, and sequence CRDTs — RGA, YATA/Yjs, Automerge — which give every character/element a stable identity between its neighbors so concurrent text edits interleave without position drift. Applications choose composition: a document is an OR-Map of field CRDTs; a cart is an OR-Map from SKU to PN-counter. The ops strip carries the recurring costs — tombstone garbage collection, metadata compaction, and whatever causal-delivery guarantee your op-based transport must uphold.
End-to-end flow
Trace a shopping cart across a partition, then a collaborative document. The cart is an OR-Map of SKU → (PN-counter, LWW flags), replicated between US and EU regions of an active-active store. A user's phone flips to flaky mobile data mid-session; their app keeps a local replica. They add two units of SKU-9: the local replica increments its own entry in the PN-counter's increment map and tags the map insertion with a new dot — no network involved, the UI updates instantly. Meanwhile a background job in EU applies a promotional free item to the same cart. When the phone reconnects, delta sync runs: the phone sends deltas covering its dots since the last acknowledged version vector; the server merges (element-wise max on counter entries, dot-set union on the map), computes what the phone hasn't seen, and sends its own deltas back. Both ends converge to a cart with the two units and the promo item. Had the user also removed SKU-3 while offline, and the promo job concurrently updated SKU-3's quantity, the add-wins OR-Map keeps SKU-3 with the promo update — a policy chosen at type-selection time, not improvised in a conflict handler.
Now the document: three editors in a Yjs-backed doc. Alice types 'hello' at the start while Bob deletes a paragraph further down and Carol, on a train with no signal, rewrites a heading. Each character insertion is identified by (client id, clock) and anchored to its left neighbor's identity; deletes mark identities as tombstoned rather than shifting positions. Alice and Bob sync through the websocket relay in milliseconds — their ops touch disjoint identity ranges, so integration is trivial. Carol reconnects an hour later; her update blob (a compressed delta of her ops) is applied by every peer, and YATA's deterministic ordering rule places her heading rewrite identically everywhere, even though everyone received it after seeing later edits. Nobody's cursor jumped, nobody resolved a merge dialog.
The repair path runs regardless: every 30 seconds each server peer picks a random neighbor, exchanges version-vector digests, and ships missing deltas — so even a message lost by a crashing relay is healed by the next anti-entropy round. Convergence never depends on any single delivery succeeding.