Conflict-Free Replicated Data Types (CRDTs): State-Based vs Op-Based Merging in Web Apps

Building real-time collaborative web applications (such as collaborative document editors, whiteboard canvases, and multi-user spreadsheets) without centralized locking requires provable strong eventual consistency. Conflict-Free Replicated Data Types (CRDTs) solve concurrent mutation anomalies mathematically by framing data models as either State-Based Convergent (CvRDT) semilattices or Operation-Based Commutative (CmRDT) causal broadcast streams, guaranteeing identical converged state across all clients.

The Mathematics of Bounded Join-Semilattices (CvRDT)

Why State-Based CRDTs converge deterministically via least-upper-bound operations:

⚡ The Monotonic Semilattice Invariant

A state-based CRDT merge function $\sqcup$ forms a bounded join-semilattice satisfying three invariant algebraic properties: Commutativity ($A \sqcup B = B \sqcup A$), Associativity ($(A \sqcup B) \sqcup C = A \sqcup (B \sqcup C)$), and Idempotence ($A \sqcup A = A$). Regardless of network packet reordering or duplicate delivery, state convergence is mathematically guaranteed.

State-Based (CvRDT) vs Operation-Based (CmRDT) Architecture Matrix

CRDT Architectural Model Network Wire Payload Transport Requirements Garbage Collection Overhead
State-Based (CvRDT)Full or Delta-State PayloadsUnreliable / Out-of-order OKSimple (State overwrites)
Operation-Based (CmRDT)Minimal Delta OperationsStrict Causal Exactly-Once DeliveryComplex (Causal buffer pruning)

Positive-Negative Counter (PN-Counter) Implementation in TypeScript

Constructing a deterministic state-based distributed counter in TypeScript:

export class PNCounter {
  constructor(
    public readonly nodeId: string,
    public p: Map<string, number> = new Map(),
    public n: Map<string, number> = new Map()
  ) {}

  public increment(amount = 1): void {
    const current = this.p.get(this.nodeId) || 0;
    this.p.set(this.nodeId, current + amount);
  }

  public decrement(amount = 1): void {
    const current = this.n.get(this.nodeId) || 0;
    this.n.set(this.nodeId, current + amount);
  }

  public read(): number {
    const totalP = Array.from(this.p.values()).reduce((sum, v) => sum + v, 0);
    const totalN = Array.from(this.n.values()).reduce((sum, v) => sum + v, 0);
    return totalP - totalN;
  }

  public merge(remote: PNCounter): PNCounter {
    const mergedP = new Map<string, number>();
    const mergedN = new Map<string, number>();
    const allKeys = new Set([...this.p.keys(), ...remote.p.keys(), ...this.n.keys(), ...remote.n.keys()]);

    allKeys.forEach((key) => {
      mergedP.set(key, Math.max(this.p.get(key) || 0, remote.p.get(key) || 0));
      mergedN.set(key, Math.max(this.n.get(key) || 0, remote.n.get(key) || 0));
    });

    return new PNCounter(this.nodeId, mergedP, mergedN);
  }
}

Engineer Resilient Real-Time Collaborative Web Applications

Eliminate merge conflicts and unlock real-time peer-to-peer web applications. Read our guide on Deterministic State Machine Replication & Raft, explore syndicated mezzanine finance on FinanceQuickly Private Credit, review rollover biomechanics on CarInjuryAttorney Forensics, or partner with our distributed systems engineers.