Beyond Code & Karma · Distributed Systems

CRDTs

Many replicas edit the same data offline, then merge in any order — and all land on the same state, with no server, no locks, no conflicts. The maths of strong eventual consistency, and the Advaita truth that beneath the many is one.

Interactive Deep Dive · Conflict-Free Replication

Edit anywhere, offline.
Merge in any order. Converge.

A CRDT — Conflict-free Replicated Data Type — is a data structure many replicas can update independently, even while disconnected, then merge automatically with no central coordinator and no conflicts. The guarantee is strong eventual consistency: once every replica has seen the same set of updates, they are byte-for-byte identical — regardless of the order in which those updates arrived.

The Secret: CRDTs don't resolve conflicts after the fact — they make conflicts impossible by construction. Every update carries enough metadata (unique ids, version vectors) that any two states can always be combined deterministically.
Replicas
Independent copies of the same data — on a phone, in another data centre, behind a flaky network. Each edits locally, no permission asked.
Eventual Consistency
Replicas may diverge for a while, but every update eventually reaches everyone, and at that point all copies agree.
Strong EC
The strong version: agreement is guaranteed the moment replicas have seen the same updates — no "last writer", no manual merge dialog.
Convergence
The destination. Scattered rivers, one sea: different edit orders, identical final state. Order-independent harmony.

Advaita — the many replicas, one reality

Advaita Vedānta — Non-Duality

In Advaita (non-duality), the countless individual souls — the jīvas — each perceive a separate, partial world. That sense of separateness is māyā. Beneath all of them is a single reality, Brahman. A CRDT is exactly this metaphysics, made mechanical: the replicas are the jīvas, the merge is the recognition that the many were always one, and convergence is the realization that — whatever the order of actions — all paths reach the same truth.

Jīvas — the replicas
Many souls, each editing offline, each certain its own partial view is the whole. The māyā of separateness.
Brahman — the merge
The union that judges none and loses none — the recognition that beneath the many copies was always one reality.
Mokṣha — convergence
Oneness, realized. No matter the sequence of acts, all converge on the one truth — the laws are niṣkāma karma.

"The commutative, associative, idempotent laws are niṣkāma karma: action whose fruit does not depend on the order it was done."

The merge obeys three laws

The entire guarantee rests on the merge function having three algebraic properties. With all three, the reachable states form a mathematical join-semilattice and convergence is forced — no locks, no consensus, no server arbitrating who wins.

Commutative
merge(a, b) === merge(b, a). Order of merging two replicas doesn't matter — peers that synced in different orders still agree.
Associative
merge(merge(a, b), c) === merge(a, merge(b, c)). Grouping doesn't matter when three or more replicas combine.
Idempotent
merge(a, a) === a. Merging the same state twice changes nothing — a duplicated or re-delivered message is harmless.

The canonical example — a G-Counter

A grow-only counter keeps one slot per replica; each replica only ever increments its own slot. The value is the sum of slots, and the merge is element-wise max — which is commutative, associative, and idempotent for free:

g-counter.js
// A G-Counter: one slot per replica — you only ever increment YOUR slot.
class GCounter {
  constructor(id, peers) {
    this.id = id;
    this.p = {};                 // replicaId → count
    peers.forEach(r => this.p[r] = 0);
  }

  inc() { this.p[this.id]++; }   // local, offline-safe

  value() {                      // sum across all slots
    return Object.values(this.p).reduce((a, b) => a + b, 0);
  }

  // merge = element-wise MAX — commutative, associative, idempotent
  merge(other) {
    for (const r in other.p)
      this.p[r] = Math.max(this.p[r], other.p[r]);
  }
}
Why max works: a slot only ever grows, so the larger of two readings is always the more up-to-date one. Taking the max per slot can never lose an increment, and re-merging the same state is a no-op. That's the whole convergence proof, in one line.

Two styles, a handful of types

There are two ways to build a CRDT, and a small zoo of standard shapes built on top of them.

State-based (CvRDT)
Ship the whole state, merge by a join function. Robust to lost or reordered messages; heavier on the wire. G-Counter is the poster child.
Op-based (CmRDT)
Broadcast individual operations that commute. Lighter, but needs reliable, roughly exactly-once delivery. OR-Set is a classic.
G- / PN-Counter
G-Counter only grows. PN-Counter = two G-Counters (P − N), so it supports decrements too.
OR-Set
Observed-Remove Set: every add carries a unique tag; remove only tombstones tags it has seen. So add wins over a concurrent remove.
LWW-Register
Last-Write-Wins: keep the value with the highest timestamp. Fine for a single scalar; destructive for a shared sequence.
RGA / Sequence
For collaborative text: every character gets a unique id and an "after" anchor, so concurrent inserts order deterministically.

The set that survives concurrent edits — an OR-Set

A naive set can't tell "I removed this" from "you re-added it concurrently". An OR-Set tags every add and only tombstones the tags it has actually observed — so add and remove commute:

or-set.js
// OR-Set (Observed-Remove Set): every add carries a unique tag.
// remove() only tombstones the tags it has SEEN — never future ones.
add(elem) {
  this.adds.push({ elem, tag: uid() });   // e.g. "x@3:A"
}

remove(elem) {
  for (const a of this.adds)
    if (a.elem === elem) this.removed.add(a.tag);  // tag-level tombstone
}

// present iff some add-tag is not in 'removed' — so add ⟂ remove
has(elem) {
  return this.adds.some(
    a => a.elem === elem && !this.removed.has(a.tag));
}

Two replicas, one total

A G-Counter on two replicas. Each increments its own slot while offline, so they disagree — then a single merge (element-wise max) converges both to the same total, no matter who counted what.

G-Counter · two replicas
⏸ Ready
Press Run to start…

Diverge, exchange, merge, converge

Diverge offline

Each replica applies local edits with no coordination. States drift apart — and that's allowed.

Exchange state

When connectivity returns, replicas gossip their state (or their ops) to one another — in any order.

Merge

The commutative, associative, idempotent merge combines any two states deterministically. No arbitration.

Converge

All replicas that have seen the same updates are now identical. Strong eventual consistency, reached.

Cheat-sheet — what each type converges on

CRDT typeConverges on
G-CounterPer-slot maxes, summed — a grow-only count
PN-CounterTwo G-Counters (P − N) — supports decrements
OR-SetPresent iff an unremoved add-tag exists — add wins
LWW-RegisterThe value with the highest timestamp
RGA / SequenceOne deterministic character order, by unique id

CRDT vs OT — two roads to multiplayer

 CRDTOT (Operational Transform)
CoordinationNone — peer-to-peer, offline-firstUsually a central server
ConflictsImpossible by construction (commutative merge)Transform ops against concurrent ops
CostMetadata: unique ids + tombstonesTricky transformation functions

The nuances that senior devs know

This is where the surface-level explainers stop. Here's what actually matters when you ship convergent state.

A delete can't simply drop an element, because a concurrent insert on another replica may reference it as its anchor — removing it outright would break convergence or resurrect the wrong order. So a delete marks the element as a tombstone: present in the structure, hidden from the visible text. Karma is never truly erased, only marked. Tombstones do accumulate, but production libraries garbage-collect them once every replica has provably seen the delete (via version vectors), and they pack runs of ids — so real documents don't grow without bound.

Treat a whole document as one LWW value and you keep only the version with the latest timestamp — so one replica's edit is silently clobbered. That's the ego overwriting another's act. LWW is perfectly fine for a single scalar (a user's status, a last-seen time) where losing the older write is acceptable, but for a shared sequence it destroys work. A sequence CRDT keeps both edits — which is exactly the difference the full visualizer lets you toggle.

Collaborative text is the hard case because position is relative — if I insert at index 3 and you delete at index 1 concurrently, our indices no longer agree. Sequence CRDTs (RGA, LSEQ, Yjs's YATA, Treedoc) give every inserted character a globally unique id (replica id + counter) and record the element it was inserted after, not a numeric index. Concurrent inserts at the same spot are ordered deterministically by comparing ids, so no character is lost and every replica computes the identical sequence.

Anywhere replicas must stay coherent without a coordinator:

SystemWhat it uses CRDTs for
Collaborative editorsFigma, Linear, and editors on Yjs / Automerge for real-time multiplayer.
Local-first appsNote-taking and mobile apps that sync when connectivity returns — no merge dialogs.
Distributed databasesRedis (Active-Active), Riak, and AntidoteDB for multi-region writes without coordination.
Caches & presenceCounters, sets, and registers replicated across regions with eventual agreement.

The Karma of the merge

The Karma Connection

Niṣkāma karma is action performed without attachment to the order or fruit of the deed. A CRDT merge is precisely this discipline: each replica acts of its own will, apart from the others — yet the merge is the impartial law of dharma, judging no order and discarding no act.

karma.js
// Each replica acts alone, of its own will…
replicaA.inc();   // your action, recorded only in your slot
replicaB.inc();   // her action, recorded only in hers

// …yet the merge judges no order and loses no act.
a.merge(b);     // === b.merge(a) — commutative: the law is impartial
a.merge(b);     // again? no change — idempotent: nothing counts twice

// all paths reach the one reading. moksha = convergence.

To overwrite another's edit with your own — last-write-wins — is the ego asserting itself. Convergence is its opposite: the surrender into a shared truth, where every act is honoured and the final reading is the same whatever the path taken to reach it. Many hands, one truth.

"As scattered rivers reach one sea, so do all replicas reach one state — order forgotten, nothing lost." 🙏

CRDT Trivia

Three questions. No coordination allowed. Your karma is watching.

Question 1
Which three properties must a CRDT merge function have to guarantee convergence?
Question 2
Two replicas of a G-Counter: A has {A:2, B:1}, B has {A:1, B:3}. After merge, what's the value?
Question 3 — 🌶️ Spicy
In a sequence CRDT, why is a deleted character kept as a tombstone instead of being removed?

The Six Truths of CRDTs

Edit Anywhere
Replicas update locally and offline, asking no permission. Availability over coordination.
The Merge Commutes
Commutative, associative, idempotent — order and grouping and repetition all stop mattering.
Strong EC
Once replicas have seen the same updates they're byte-for-byte identical. No "last writer" needed.
Ids, Not Indices
Unique ids and "after" anchors let concurrent inserts order deterministically — nothing is lost.
Mark, Don't Erase
Deletes are tombstones, kept as anchors so insert and delete commute. Nothing done is truly forgotten.
Advaita — One Truth
The many replicas were always one. Convergence is mokṣha: every path reaches the same reality.

कर्म करो, फल की चिंता मत करो