CRDTs Explained: Conflict-Free Data Types for Distributed Apps

Philip Rehberger Aug 14, 2026 6 min read

Build collaborative apps without coordination using CRDTs. Counters, sets, and text — when each variant fits.

Conflict-free Replicated Data Types — CRDTs — solve a problem that comes up the moment you let multiple users edit the same data without strong coordination: how do you merge concurrent edits without making someone's changes disappear?

CRDTs are data structures designed so that any two replicas, no matter what changes they have seen, can be merged deterministically — without coordination, without "last write wins," and without losing data. They are the math behind collaborative editors, offline-first applications, and certain kinds of distributed databases.

The Problem They Solve

Imagine a shared counter. Two users increment it at the same time. The naive implementation reads the current value, adds one, and writes back. With concurrent edits, this is the classic lost-update problem — both reads see the old value, both writes store the same incremented value, and you have lost one of the increments.

The "fix" of last-write-wins is worse: an increment is treated as overwriting the counter, so concurrent increments produce one increment instead of two. Strong consistency through coordination — locking, transactions, consensus — solves it, but only at the cost of latency and availability.

A CRDT counter solves the problem without coordination. Each replica tracks its own increments. Merging two replicas adds them. The order of operations does not matter, the order of merges does not matter, and the result is always correct.

Two Families: State-Based and Operation-Based

State-based CRDTs (CvRDTs) ship the whole state between replicas. The merge function combines two states into a new state that is at least as current as both. Merge must be commutative (merge(a, b) = merge(b, a)), associative (merge(a, merge(b, c)) = merge(merge(a, b), c)), and idempotent (merge(a, a) = a). These properties guarantee that any replica that has seen the same set of states ends up with the same value.

Operation-based CRDTs (CmRDTs) ship individual operations rather than full state. Operations must be commutative — applying them in any order produces the same result. The transport layer must deliver each operation exactly once (no duplicates, no drops). This is harder to guarantee than state-based merging but ships less data.

Most production systems use state-based CRDTs because the transport guarantees are easier to meet. Operation-based CRDTs are common in systems with a reliable broker (like Yjs in collaborative editors) where exactly-once delivery is part of the platform.

Concrete Types

G-Counter (grow-only counter). A counter that only increases. Each replica tracks its own increment count. The value is the sum across all replicas. Merge is element-wise max.

Replica A: { A: 3, B: 0 }   value = 3
Replica B: { A: 0, B: 5 }   value = 5
Merge:     { A: 3, B: 5 }   value = 8

PN-Counter. Two G-Counters — one for increments, one for decrements. The value is increments minus decrements. Supports a counter that goes up or down.

G-Set (grow-only set). A set that you can only add to. Merge is the union of both sets.

2P-Set (two-phase set). Tracks both an "added" set and a "removed" set. An element is in the set if it has been added and not removed. Once removed, it cannot be re-added.

OR-Set (observed-removed set). More flexible than 2P-Set: elements can be re-added after removal. Adds carry a unique tag; removes target specific tags. The most common set CRDT in production.

LWW-Register. A single value with a timestamp. The merged value is the one with the later timestamp. Last-write-wins, but mathematically rigorous about which write is later. Vulnerable to clock skew across replicas.

Sequence CRDTs (RGA, Treedoc, Yjs). Lists where elements can be inserted at arbitrary positions concurrently. These are the structures behind real-time collaborative editors.

A Practical Example: Shopping Cart

A canonical CRDT example is a shopping cart that can be edited on multiple devices offline. Naive merging silently drops the items added on the device that was offline longest. An OR-Set CRDT preserves them.

// Each cart is an OR-Set of (productId, uniqueAddTag)
cartOnPhone = {
  "headphones-pro": ["tag-001"],
  "phone-case": ["tag-003"],
}
cartOnLaptop = {
  "headphones-pro": ["tag-001"],
  "charger": ["tag-005"],
}

// Merge: union of all (productId, tag) pairs
merged = {
  "headphones-pro": ["tag-001"],   // same item, same tag — dedup
  "phone-case": ["tag-003"],       // added on phone
  "charger": ["tag-005"],          // added on laptop
}

Both items are preserved. Remove operations carry tag references, so removing an item on one device removes only the additions that were already replicated, leaving room for concurrent additions to be merged in.

Where CRDTs Win

  • Real-time collaboration (Figma, Notion, Linear, Google Docs). Multiple users editing simultaneously, low latency requirement, no central coordinator on the hot path.
  • Offline-first mobile and web apps. Users edit while offline; changes sync and merge when they reconnect.
  • Geographically distributed databases. Some database engines (Riak, AntidoteDB, Redis Enterprise with active-active) expose CRDTs as primary data types.
  • IoT and edge devices. Devices that may be partitioned for long periods need to reconcile state when they reconnect.

Where CRDTs Are the Wrong Answer

  • When you actually want strong consistency. CRDTs are eventually consistent. If "the bank balance must be correct at all times" is your requirement, you want consensus, not CRDTs.
  • When your data model is highly relational. CRDTs work best on simple data structures (counters, sets, registers, lists). Complex relational invariants ("an order cannot have more items than the user is allowed") are hard to express.
  • When the operational complexity is unjustified. A small SaaS with two replicas and clear primary-replica routing does not need CRDTs. Add them when concurrent editing is actually a product requirement.

Implementation Options

You rarely build CRDTs from scratch. Production-quality libraries exist:

Library Use case
Yjs Collaborative editing (web, mobile)
Automerge JSON-like documents with conflict-free merging
Riak's CRDTs Database-level counters, sets, maps
Redis Enterprise CRDB Multi-master Redis with CRDTs
Hazelcast PN-Counter JVM-level distributed counters

Yjs and Automerge are the practical choice for application-level use. For database-level use, picking a database that ships CRDTs natively is usually better than rolling your own on top of a non-CRDT store.

What CRDTs Cost

  • Storage overhead. Most CRDTs carry per-element metadata (tags, versions, tombstones). A set of 100 items can easily be many kilobytes of state.
  • Compaction complexity. Tombstones accumulate. Without periodic compaction, CRDTs grow unboundedly.
  • Reasoning load. Engineers have to understand the data type's merge semantics. "I added it, why is it not there?" usually has a non-obvious answer.

The cost is justified when the alternative — coordination, conflict UI, or silent data loss — is worse. For collaborative editing and offline-first applications, CRDTs are not a clever choice; they are the only choice that actually works.


Designing a feature where multiple users edit the same data and the conflict-handling story matters? We help teams pick the right consistency model for the product. scopeforged.com

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.