Build CRDT intuition with grow-only and PN counters plus an observed-remove set, then distinguish algebraic convergence from invariant preservation and arbitrary-object conflict freedom.

CRDT Foundations: Counters, Sets, Registers, Monotonic State, and Convergence

CRDTs encode conflict semantics into a data type. This lesson demonstrates join/merge properties and set metadata while making clear that convergence does not erase business invariants or operational cost.

Advanced120–155 minutesCRDT convergence labPython 3.13+ · standard libraryVendor-neutral · free/local mandatory pathLast reviewed: August 2026
01

Explain state-based and operation-based CRDT intuition without claiming arbitrary replicated objects are automatically conflict-free.

02

Demonstrate semilattice-style merge properties using a grow-only counter and PN-counter.

03

Explain observed-remove/add-remove set metadata and why removals require more information than simple set union.

04

Distinguish convergence from domain correctness, invariant preservation, storage cost, and authorization.

1. CRDT means the conflict semantics are designed into the data type

A Conflict-Free Replicated Data Type (CRDT) is constructed so replicas can accept certain updates independently and later converge without coordination, assuming the CRDT's delivery/state assumptions hold. There are two broad families. State-based (convergent) CRDTs periodically merge replica states using a join operation. Operation-based (commutative) CRDTs disseminate operations under delivery assumptions chosen by the design.

The key phrase is designed data type. Renaming an arbitrary shopping-order object “CRDT” does not make payment, inventory, and shipping invariants merge safely.

2. State-based merge needs algebraic safety

For a state-based CRDT, the merge behaves like a least-upper-bound in a join-semilattice. At the practical level, the merge should be commutative (merge(A,B)=merge(B,A)), associative, and idempotent (merge(A,A)=A). Those properties let replicas receive states repeatedly and in different groupings without changing the eventual result.

A grow-only counter (G-Counter) stores one non-decreasing component per replica. A replica increments only its own component. Merge takes the component-wise maximum; value is the sum. A positive-negative counter (PN-Counter) uses two grow-only counters, one for increments and one for decrements, then subtracts their totals.

Type State/operation idea Merge intuition Important limitation
G-Counter per-replica non-decreasing counts component-wise max cannot decrement
PN-Counter P increments and N decrements merge P and N independently may violate external bounds such as stock >= 0 without coordination
OR-Set adds carry unique tags; remove records observed tags union add/remove metadata metadata/tombstone growth; concurrent add/remove semantics must be chosen
Register one logical value with a resolver depends on register type LWW register converges by dropping competing values

3. Removes are harder than adds

A grow-only set converges by union, but it cannot remove. A two-phase set records additions and removals but does not allow re-adding a removed element. An Observed-Remove Set (OR-Set) attaches unique identities to add operations. A remove only removes add-tags it has observed, so a truly concurrent new add can survive. Different CRDT set variants deliberately choose different concurrent add/remove semantics.

This metadata is the cost of preserving intent. Garbage-collecting tags or tombstones safely requires causal/stability knowledge; deleting them too early can resurrect elements after delayed replicas reappear.

4. AtlasMart lab: merge counters and an observed-remove set

Mandatory lab environment

Python 3.13+ standard library only. The generated lab was verified with Python 3.13.5. No database server, Docker, cloud account, paid feature, credential, firewall change, clock manipulation, process killing, or destructive failure injection is required. All conflicts, partitions, retries, and failures are deterministic in-memory simulations.

The lab builds a state-based G-Counter, derives a PN-counter value, then implements a small OR-Set-style simulator with unique add tags and an observed-remove set. It verifies commutativity/idempotence for the deterministic merges.

python · AtlasMart deterministic simulation
from copy import deepcopy

print("STATE-BASED G-COUNTER")
def gc_inc(state, replica, n=1):
    state[replica] = state.get(replica, 0) + n

def gc_merge(a, b):
    return {k: max(a.get(k, 0), b.get(k, 0)) for k in set(a) | set(b)}

def gc_value(state): return sum(state.values())

A = {"A": 0, "B": 0}; B = deepcopy(A)
gc_inc(A, "A", 3); gc_inc(B, "B", 2)
print("before merge A:", A, "B:", B)
m1 = gc_merge(A, B); m2 = gc_merge(B, A)
print("merge A,B:", m1, "value:", gc_value(m1))
print("commutative:", m1 == m2)
print("idempotent:", gc_merge(m1, m1) == m1)

print("\nPN-COUNTER = TWO G-COUNTERS")
p = {"A": 4, "B": 1}; n = {"A": 1, "B": 2}
print("value:", gc_value(p) - gc_value(n), "P:", p, "N:", n)

print("\nOBSERVED-REMOVE SET WITH UNIQUE ADD TAGS")
def or_add(state, element, tag):
    state["adds"].setdefault(element, set()).add(tag)
def or_remove(state, element):
    state["removes"].update(state["adds"].get(element, set()))
def or_merge(a, b):
    out = {"adds": {}, "removes": set(a["removes"]) | set(b["removes"])}
    for e in set(a["adds"]) | set(b["adds"]):
        out["adds"][e] = set(a["adds"].get(e, set())) | set(b["adds"].get(e, set()))
    return out
def or_value(state):
    return sorted(e for e,tags in state["adds"].items() if tags - state["removes"])

s0 = {"adds": {}, "removes": set()}
or_add(s0, "sku-7", "A:1")
Aset = deepcopy(s0); Bset = deepcopy(s0)
or_remove(Aset, "sku-7")       # removes the observed A:1 add
or_add(Bset, "sku-7", "B:1")   # concurrent new add
sm = or_merge(Aset, Bset)
print("A value:", or_value(Aset), "B value:", or_value(Bset))
print("merged add-wins-for-concurrent-new-tag value:", or_value(sm))
print("merge idempotent:", or_value(or_merge(sm, sm)) == or_value(sm))

print("\nREGISTER WARNING")
print("a last-write-wins register can converge, but convergence does not mean both business edits were preserved")
print("CRDT merge laws apply to the designed state/operations, not arbitrary objects by naming convention")
Expected evidence

Independent counter increments converge to five after component-wise-max merge, and repeated merge is idempotent. The OR-Set removes an observed add while preserving a concurrent new add with a different tag. This is a teaching implementation, not a complete production CRDT library; safe metadata compaction, causal delivery, persistence, and replica membership require additional design.

5. Convergence does not preserve every business invariant

A PN-counter can converge perfectly while inventory becomes negative if disconnected regions each decrement against the same logical stock. A set can converge while an unauthorized principal adds itself. A register can converge while losing one user's text edit. CRDTs remove a class of coordination needed for convergence; they do not remove authorization, bounded-resource, uniqueness, referential, or workflow invariants.

Where an invariant is not closed under the CRDT merge, coordination, escrow/allocation, invariant-preserving decomposition, or a different data type is still required.

6. Deliberately wrong approach: “all operations commute if retries are idempotent”

Idempotence means repeating an operation does not change the intended effect after the first application. Commutativity means changing the order of two distinct operations does not change the result. They are different properties. set price=90 can be idempotent but does not commute with apply 10% discount. CRDT operation designs need the exact delivery and algebraic assumptions they claim.

7. Production judgment and bridge

Choose a CRDT when disconnected/low-coordination updates are valuable, the data type's conflict semantics match the product, and the metadata/storage cost is understood. Test convergence under duplicate, reordering, delay, restart, membership changes, and metadata compaction. Observe state size, tombstone/tag growth, merge CPU, anti-entropy lag, divergent-state duration, and invariant violations outside the CRDT.

The final chapter lesson turns to a related but distinct property: idempotency. It is often the right response to duplicate delivery and ambiguous retries even when the underlying data is not a CRDT.

Check your understanding

  1. What properties make a state-based merge robust to repetition and order?
  2. Why does a G-Counter use per-replica components?
  3. Why does an OR-Set need add identities/tags?
  4. Does CRDT convergence guarantee stock never goes negative?
  5. How is idempotence different from commutativity?
Review the answers

1. Commutativity, associativity, and idempotence, with states forming the required monotonic/join structure.

2. Each replica can increase its own monotonic component independently; component-wise maximum merge preserves every observed increment.

3. A remove must identify which observed additions it removes so a truly concurrent new add can be distinguished.

4. No. Bounded-resource invariants can still require coordination or escrow/allocation even when replica states converge.

5. Idempotence concerns repeating the same operation; commutativity concerns reordering distinct operations.

References

Foundational claims use primary research/specifications where practical. Product documentation is used only as a current implementation example and is not required for the mandatory labs.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.