Conflict-Free Replicated Data Types (CRDTs): Merging Concurrent Writes Without Coordination
Core concept
When two nodes in a distributed system accept writes simultaneously and then sync up, you have a conflict: both versions claim to be "current." The usual fix is to prevent concurrent writes entirely — but that requires coordination (asking nodes to agree before accepting a write), which costs latency and availability. CRDTs (data structures designed so any two states can always be merged) sidestep this entirely: they're designed so every possible merge of two states always produces a single, deterministic result, with no locks, no leader, and no "who wins?" logic needed. The key insight is that you constrain what operations are allowed so that the order you apply them never matters — and therefore no conflict can ever arise.
Diagram
flowchart LR
C["Client Write: +1"] --> N1["Node A\nCounter = 3"]
C2["Client Write: +1"] --> N2["Node B\nCounter = 5"]
N1 -->|"sync: share state"| M["Merge:\nmax per node"]
N2 -->|"sync: share state"| M
M --> R["Final Counter = 8\n(deterministic)"]
Concrete real-world example
Imagine a collaborative document editor where two users add text simultaneously while offline. When they reconnect, both edits must survive — neither should be "lost." Systems like Figma (a browser-based design tool) and Redis (a fast in-memory data store) use CRDT-based data structures for exactly this. A simple example is a G-Counter (a grow-only counter, additions only): each node keeps its own slot in a small array and only increments its own slot. Merging two counters means taking the max value of each slot and summing them — you can never "double-count" because each node owns exactly one slot, and max is idempotent (applying it twice gives the same result as once).
One trade-off / gotcha
CRDTs only work for operations that are commutative (order doesn't matter) and idempotent (duplicates don't change the result). This limits what you can build: a counter that allows subtraction ("decrement") is far trickier — you need a separate "negative" counter alongside your positive one (called a PN-Counter, a two-part counter tracking additions and removals), and you still can't prevent a balance from going below zero without coordination. The more expressive you need your data type to be, the more CRDT state you carry around, which can balloon in size over time (this is called tombstone accumulation, where deleted elements still occupy space as markers).
An interview-style question to ponder
You're designing a "likes" counter for a social media post. Millions of users can like and unlike the same post. You want the counter to be always-available and eventually consistent across regions. Should you use a CRDT PN-Counter for this, and what's the biggest practical risk?
Stuck? Show a hint
Think about what happens to the size of a PN-Counter over time when millions of users participate — and whether a "like" truly needs the precision a CRDT promises.
Show answer
A CRDT PN-Counter can work, but the biggest practical risk is state size explosion, which makes it a poor fit at extreme scale without mitigation.
- A PN-Counter tracks a per-node positive and negative vector. With a handful of data-center nodes (say, 5 regions), the vector stays tiny — merging 5 slots is trivial. That's the happy path.
- But "likes" are a high-cardinality, user-driven operation. If you naively assign a slot per user rather than per node, the vector grows to millions of entries and syncing it across regions becomes a network and memory nightmare.
- The practical fix is to funnel all writes through a small, fixed number of regional aggregators (each region gets one slot), not per-user slots. Now the vector is always size 5 regardless of user count, and you trade fine-grained attribution for manageability.
- But why not just use a single atomic counter with a strong leader? Because a single leader becomes a bottleneck and a failure point: every like from every region must round-trip to one node, adding 100–300 ms of latency for distant users and killing availability if that node goes down. The CRDT approach lets each region accept likes locally and sync asynchronously.
- Watch out: even with node-level slots, tombstones from "unlike" events accumulate — periodically compacting or resetting the counter (with a coordinated snapshot) is necessary in production.