Back to All Concepts
AlgorithmsDistributed SystemsCollaborationExpert

CRDTs (Real-time Collaboration)

Conflict-free Replicated Data Types enable distributed systems to achieve eventual consistency without coordination, powering Google Docs, Figma, and collaborative editing through mathematically proven merge algorithms.

What are CRDTs?

CRDTs (Conflict-free Replicated Data Types) are data structures that can be updated independently on different devices and always converge to the same state when merged.

Key insight: Mathematical properties guarantee conflicts resolve automatically—no central server needed!

The Collaboration Problem

Scenario: Google Docs with 2 users editing simultaneously

User A types: "Hello"  (position 0)
User B types: "World"  (position 0)

Problem: How do we merge without losing data?

Naive merge: "World" (B overwrites A) ❌
Lock-based: B waits for A to finish (slow) ❌
CRDT: "HelloWorld" or "WorldHello" (both valid, deterministic) ✅
Click to expand code...

Why CRDTs?

Traditional Approaches

1. Locking (pessimistic)

User A acquires lock → edits → releases lock
User B waits... waits... finally gets lock

Problem: Poor UX, doesn't work offline
Click to expand code...

2. Last-Write-Wins (LWW)

User A: "Hello" (timestamp: 12:00:00)
User B: "World" (timestamp: 12:00:01)

Result: "World" (B wins)
Problem: A's work is lost! ❌
Click to expand code...

3. Operational Transformation (OT)

Used by Google Docs (historically)
Requires central server to transform operations
Complex edge cases

Example:
User A: Insert("x", position=5)
User B: Delete(position=3)
Server transforms: "A's insert now at position=4" (shifted by delete)

Problem: Requires server, complex implementation
Click to expand code...

CRDT Solution

No locking
No central coordination
Always converges to same state
Works offline
P2P friendly
Click to expand code...

CRDT Types

1. State-based CRDTs (CvRDT)

Merge entire state. Send full state to peers, merge function guarantees convergence.

typescript
// G-Set (Grow-only Set)
class GSet<T> {
  private elements: Set<T> = new Set();
  
  add(element: T): void {
    this.elements.add(element);
  }
  
  // Merge function (commutative, associative, idempotent)
  merge(other: GSet<T>): GSet<T> {
    const result = new GSet<T>();
    // Union of both sets
    for (const elem of this.elements) {
      result.elements.add(elem);
    }
    for (const elem of other.elements) {
      result.elements.add(elem);
    }
    return result;
  }
  
  has(element: T): boolean {
    return this.elements.has(element);
  }
  
  toArray(): T[] {
    return Array.from(this.elements);
  }
}

// Usage
const set1 = new GSet<string>();
set1.add("apple");
set1.add("banana");

const set2 = new GSet<string>();
set2.add("banana");
set2.add("cherry");

const merged = set1.merge(set2);
// Result: ["apple", "banana", "cherry"]
// Order doesn't matter: merge(set1, set2) === merge(set2, set1)
Click to expand code...

2. Operation-based CRDTs (CmRDT)

Send operations. Broadcast each operation, apply in causal order.

typescript
// OR-Set (Observed-Remove Set)
class ORSet<T> {
  private elements: Map<T, Set<string>> = new Map();  // element → unique tags
  
  add(element: T, uniqueTag: string = uuid()): void {
    if (!this.elements.has(element)) {
      this.elements.set(element, new Set());
    }
    this.elements.get(element)!.add(uniqueTag);
  }
  
  remove(element: T): void {
    // Remove all observed tags for this element
    this.elements.delete(element);
  }
  
  has(element: T): boolean {
    const tags = this.elements.get(element);
    return tags !== undefined && tags.size > 0;
  }
  
  merge(other: ORSet<T>): ORSet<T> {
    const result = new ORSet<T>();
    
    // Union of all elements with their tags
    for (const [elem, tags] of this.elements) {
      for (const tag of tags) {
        result.add(elem, tag);
      }
    }
    for (const [elem, tags] of other.elements) {
      for (const tag of tags) {
        result.add(elem, tag);
      }
    }
    
    return result;
  }
}

// Example: Concurrent add/remove
const set1 = new ORSet<string>();
set1.add("item", "tag1");

const set2 = new ORSet<string>();
set2.add("item", "tag1");  // Same item
set2.remove("item");       // But then removed

const merged = set1.merge(set2);
merged.has("item");  // false - remove wins (both observed it before removal)
Click to expand code...

Text Editing CRDTs

Problem: Collaborative Text Editing

Initial: "Hello"

User A inserts "Beautiful " at position 0:
Result: "Beautiful Hello"

User B (simultaneously) inserts "World" at position 6:
Result: "Hello World"

How to merge?
Click to expand code...

RGA (Replicated Growable Array)

typescript
interface Character {
  id: string;           // Unique ID
  value: string;        // Actual character
  timestamp: number;    // For ordering
  left: string | null;  // ID of character to the left
  right: string | null; // ID of character to the right
  deleted: boolean;
}

class RGA {
  private chars: Map<string, Character> = new Map();
  private head: string | null = null;
  
  insert(value: string, afterId: string | null, id: string = uuid(), timestamp: number = Date.now()): void {
    const char: Character = {
      id,
      value,
      timestamp,
      left: afterId,
      right: null,
      deleted: false
    };
    
    if (afterId) {
      const leftChar = this.chars.get(afterId)!;
      char.right = leftChar.right;
      leftChar.right = id;
      
      if (char.right) {
        this.chars.get(char.right)!.left = id;
      }
    } else {
      // Insert at beginning
      char.right = this.head;
      if (this.head) {
        this.chars.get(this.head)!.left = id;
      }
      this.head = id;
    }
    
    this.chars.set(id, char);
  }
  
  delete(id: string): void {
    const char = this.chars.get(id);
    if (char) {
      char.deleted = true;  // Tombstone (don't actually remove)
    }
  }
  
  toString(): string {
    let result = '';
    let current = this.head;
    
    while (current) {
      const char = this.chars.get(current)!;
      if (!char.deleted) {
        result += char.value;
      }
      current = char.right;
    }
    
    return result;
  }
  
  merge(other: RGA): void {
    // Merge all characters from other
    for (const [id, char] of other.chars) {
      if (!this.chars.has(id)) {
        this.chars.set(id, { ...char });
      } else {
        // Conflict resolution: use timestamp
        const existing = this.chars.get(id)!;
        if (char.timestamp > existing.timestamp) {
          this.chars.set(id, { ...char });
        }
      }
    }
    
    // Rebuild ordering
    this.rebuildOrder();
  }
  
  private rebuildOrder(): void {
    // Rebuild linked list based on timestamps and IDs
    // (Simplified - real implementation more complex)
  }
}

// Example usage
const doc1 = new RGA();
doc1.insert('H', null, 'id1');
doc1.insert('e', 'id1', 'id2');
doc1.insert('l', 'id2', 'id3');
doc1.insert('l', 'id3', 'id4');
doc1.insert('o', 'id4', 'id5');

console.log(doc1.toString());  // "Hello"

// Concurrent edits
const doc2 = new RGA();
doc2.merge(doc1);

doc1.insert(' ', 'id5', 'id6');
doc1.insert('W', 'id6', 'id7');
doc1.insert('o', 'id7', 'id8');
doc1.insert('r', 'id8', 'id9');
doc1.insert('l', 'id9', 'id10');
doc1.insert('d', 'id10', 'id11');

doc2.insert('!', 'id5', 'id12');

doc1.merge(doc2);
console.log(doc1.toString());  // "Hello World!" (deterministic merge)
Click to expand code...

Real-World Implementations

Yjs (JavaScript CRDT Library)

javascript
import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';

// Create shared document
const ydoc = new Y.Doc();

// Create shared text type
const ytext = ydoc.getText('mytext');

// P2P sync via WebRTC
const provider = new WebrtcProvider('my-room-name', ydoc);

// Local edits
ytext.insert(0, 'Hello');
ytext.insert(5, ' World');

// Observ changes (from local and remote)
ytext.observe(event => {
  console.log('Text changed:', ytext.toString());
});

// Delete range
ytext.delete(0, 5);  // Remove "Hello"

// Collaborative cursors
const awareness = provider.awareness;
awareness.setLocalStateField('cursor', { position: 5, color: '#ff0000' });

awareness.on('change', () => {
  const states = awareness.getStates();
  states.forEach((state, clientId) => {
    console.log(`Client ${clientId} cursor at:`, state.cursor);
  });
});
Click to expand code...

Automerge (Multi-platform CRDT)

javascript
import * as Automerge from '@automerge/automerge';

// Create document
let doc1 = Automerge.init();

// Make changes
doc1 = Automerge.change(doc1, doc => {
  doc.text = new Automerge.Text();
  doc.text.insertAt(0, ...'Hello'.split(''));
});

// Fork document (simulate 2 users)
let doc2 = Automerge.clone(doc1);

// Concurrent edits
doc1 = Automerge.change(doc1, doc => {
  doc.text.insertAt(5, ...' World'.split(''));
});

doc2 = Automerge.change(doc2, doc => {
  doc.text.insertAt(0, ...'Hi '.split(''));
});

// Merge
doc1 = Automerge.merge(doc1, doc2);

console.log(doc1.text.toString());  // "Hi Hello World"

// Time travel
const history = Automerge.getHistory(doc1);
history.forEach((state, index) => {
  console.log(`Version ${index}:`, state.snapshot.text.toString());
});
Click to expand code...

Advanced CRDT Types

Counter CRDT

typescript
class GCounter {
  private counts: Map<string, number> = new Map();  // nodeId → count
  
  constructor(private nodeId: string) {
    this.counts.set(nodeId, 0);
  }
  
  increment(amount: number = 1): void {
    const current = this.counts.get(this.nodeId) || 0;
    this.counts.set(this.nodeId, current + amount);
  }
  
  value(): number {
    let total = 0;
    for (const count of this.counts.values()) {
      total += count;
    }
    return total;
  }
  
  merge(other: GCounter): GCounter {
    const result = new GCounter(this.nodeId);
    
    // Take max count for each node
    const allNodes = new Set([...this.counts.keys(), ...other.counts.keys()]);
    
    for (const nodeId of allNodes) {
      const thisCount = this.counts.get(nodeId) || 0;
      const otherCount = other.counts.get(nodeId) || 0;
      result.counts.set(nodeId, Math.max(thisCount, otherCount));
    }
    
    return result;
  }
}

// Example: Distributed counter
const counter1 = new GCounter('node1');
counter1.increment(5);

const counter2 = new GCounter('node2');
counter2.increment(3);

const merged = counter1.merge(counter2);
console.log(merged.value());  // 8
Click to expand code...

LWW-Element-Set (Last-Write-Wins)

typescript
class LWWSet<T> {
  private adds: Map<T, number> = new Map();     // element → timestamp
  private removes: Map<T, number> = new Map();  // element → timestamp
  
  add(element: T, timestamp: number = Date.now()): void {
    this.adds.set(element, timestamp);
  }
  
  remove(element: T, timestamp: number = Date.now()): void {
    this.removes.set(element, timestamp);
  }
  
  has(element: T): boolean {
    const addTime = this.adds.get(element) || 0;
    const removeTime = this.removes.get(element) || 0;
    
    // Element exists if added after last removal (or never removed)
    return addTime > removeTime;
  }
  
  merge(other: LWWSet<T>): LWWSet<T> {
    const result = new LWWSet<T>();
    
    // Merge adds (take latest timestamp)
    for (const [elem, time] of this.adds) {
      result.adds.set(elem, time);
    }
    for (const [elem, time] of other.adds) {
      const existing = result.adds.get(elem) || 0;
      result.adds.set(elem, Math.max(existing, time));
    }
    
    // Merge removes (take latest timestamp)
    for (const [elem, time] of this.removes) {
      result.removes.set(elem, time);
    }
    for (const [elem, time] of other.removes) {
      const existing = result.removes.get(elem) || 0;
      result.removes.set(elem, Math.max(existing, time));
    }
    
    return result;
  }
}
Click to expand code...

Real-World Applications

Figma (Collaborative Design)

CRDTs used for:
- Object properties (position, color, size)
- Layer ordering
- Selection state
- Comments

Benefits:
- Real-time collaboration
- Offline editing
- No central bottleneck
- 60 FPS performance
Click to expand code...

Redis (Conflict-free Replication)

Redis CRDTs:
- Counters (page views across regions)
- Sets (unique visitors)
- Registers (configuration values)

Multi-region setup:
Region 1: counter.increment(5)
Region 2: counter.increment(3)
Merge: 8 (consistent everywhere)
Click to expand code...

Apple Notes (Multi-device Sync)

CRDT-based sync:
- Edit on iPhone (offline)
- Edit on Mac (simultaneously)
- Both sync to iCloud
- Changes merge automatically
- No conflicts, no data loss
Click to expand code...

Pros & Cons

Pros:

  • No coordination needed
  • Works offline
  • P2P friendly
  • Always converges
  • No conflicts (by definition)
  • Strong mathematical guarantees

Cons:

  • Memory overhead (tombstones)
  • Cannot express all data types
  • Complex to implement correctly
  • Requires causal ordering
  • May need garbage collection

Interview Tips 💡

When discussing CRDTs in system design interviews:

  1. Problem identification: "For collaborative editing, we need concurrent updates without locking..."
  2. CRDT choice: "Use Yjs for text collaboration—proven, production-ready..."
  3. Offline support: "CRDTs enable offline edits that sync automatically when online..."
  4. Comparison: "Unlike OT, CRDTs don't need central server for transformation..."
  5. Real examples: "Figma uses CRDTs for real-time design collaboration..."
  6. Trade-offs: "CRDTs use more memory, but guarantee convergence..."

Related Concepts

About ScaleWiki

ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.

Read more about our Editorial Guidelines & Authorship.

Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.

Related Articles