CRDTs: Data Structures That Never Conflict
Two users add items to the same shopping cart. On different servers. At the same time. Network is down between them.
Later, the network heals. Now what?
With vector clocks, you can detect that both writes happened concurrently. But detection isn’t resolution. Someone still has to decide what the final cart looks like.
What if the data structure itself knew how to merge?
The Idea#
CRDTs (Conflict-free Replicated Data Types) are data structures designed so that any two states can always be merged into a consistent result. No coordination needed. No conflicts possible. Mathematically guaranteed.
The trick: design operations that are commutative (order doesn’t matter), associative (grouping doesn’t matter), and idempotent (duplicates don’t matter).
If your operations have these properties, replicas can apply updates in any order, receive duplicates, miss messages temporarily, and still converge to the same state.
G-Counter: The Simplest CRDT#
A regular counter breaks in distributed systems. Node A increments to 5. Node B increments to 5. They sync. Is the answer 5 or 10?
A G-Counter (Grow-only Counter) fixes this. Each node maintains its own count:
Node A: {A: 3, B: 0}
Node B: {A: 0, B: 2}
To merge, take the max of each slot:
Merged: {A: 3, B: 2}
Total: 5
No conflict. Both increments preserved. Order didn’t matter.
Want to support decrements? Use a PN-Counter: two G-Counters, one for increments, one for decrements. Subtract to get the value.
OR-Set: Add-Wins Set#
Sets are trickier. What if Node A adds “milk” while Node B removes “milk”?
An OR-Set (Observed-Remove Set) solves this with unique tags. Every add creates a new tag. Remove only removes tags you’ve seen.
Node A adds "milk" → {milk: [tag-1]}
Node B sees tag-1, removes "milk" → {}
Node A adds "milk" again → {milk: [tag-2]}
Merge → {milk: [tag-2]} // milk is back!
The “add” that Node B never saw wins. Hence “add-wins” or “observed-remove.” You can only remove what you’ve observed.
LWW-Register: Last Writer Wins#
Sometimes you just want a single value. LWW-Register (Last-Writer-Wins Register) attaches a timestamp to each write. Highest timestamp wins.
Node A writes "red" at T=5
Node B writes "blue" at T=7
Merge → "blue" wins
Simple, but dangerous. If clocks are skewed, “last” might not mean what you think. Use with caution. Works best when writes are rare and clock drift is small.
Real World Usage#
Shopping carts: Amazon’s Dynamo paper famously described using CRDTs for carts. Add items on any node, merge later. Customer might see duplicate adds temporarily, but nothing gets lost.
Collaborative editing: Google Docs and Figma use CRDT-like structures. Multiple users type simultaneously, changes merge without conflicts.
Distributed counters: Redis has CRDT-based counters for multi-region deployments. Like counts, view counts, anything where “roughly right” beats “globally coordinated.”
The Trade-offs#
CRDTs aren’t magic. They have real costs:
Limited operations. You can’t build a CRDT for every data structure. Some operations fundamentally conflict. A bank balance needs coordination.
Storage overhead. G-Counters grow with nodes. OR-Sets keep tombstones. Metadata accumulates.
Semantic gaps. “Add wins” or “last writer wins” might not match your business logic. Sometimes you need application-level merge.
What I’m Learning#
CRDTs complete the picture I started with vector clocks and gossip. Vector clocks detect concurrency. Gossip spreads state. CRDTs merge without conflict.
Together, they form a toolkit for building AP systems that don’t need a leader. No consensus. No coordination. Just math that guarantees convergence.
The insight that changed my thinking: conflict resolution isn’t always a runtime decision. Sometimes you can design it into the data structure itself. The structure’s rules become the resolution policy.
Not every problem fits a CRDT. But when it does, you get availability and consistency without the coordination tax.
What data in your system could be modeled as a CRDT?