SalakCode SalakCode
Architecture

CRDTs

Conflict-free replicated data types

Advanced
architecture collaboration sync distributed

Definition

CRDTs (Conflict-free Replicated Data Types) are data structures that can be replicated across multiple nodes in a distributed system and updated independently, while guaranteeing that all replicas will eventually converge to the same state without requiring consensus or conflict resolution.

The Problem

Concurrent Editing

User A: "Hello" → "Hello World"
User B: "Hello" → "Hi there"

Without CRDTs: Which version wins?
Last-write-wins? Lose data!
Operational Transform? Complex!

With CRDTs: Merge automatically
Result: "Hello World" + "Hi there" = "Hi World"

Basic Concepts

State-Based CRDTs

// G-Counter (Grow-only Counter)
class GCounter {
  constructor() {
    this.state = new Map(); // node -> value
  }
  
  increment(nodeId) {
    const current = this.state.get(nodeId) || 0;
    this.state.set(nodeId, current + 1);
  }
  
  value() {
    return Array.from(this.state.values())
      .reduce((a, b) => a + b, 0);
  }
  
  merge(other) {
    for (const [node, value] of other.state) {
      const current = this.state.get(node) || 0;
      this.state.set(node, Math.max(current, value));
    }
  }
}

// Usage
const counterA = new GCounter();
counterA.increment('A');
counterA.increment('A');

const counterB = new GCounter();
counterB.increment('B');

counterA.merge(counterB);
console.log(counterA.value()); // 3

Operation-Based CRDTs

// Causal broadcast ensures ordering
class TextCRDT {
  constructor() {
    this.text = [];
    this.operations = [];
  }
  
  insert(char, position) {
    const op = {
      type: 'insert',
      char,
      position,
      timestamp: generateTimestamp(),
      nodeId: this.nodeId
    };
    
    this.apply(op);
    this.broadcast(op);
  }
  
  apply(op) {
    if (op.type === 'insert') {
      this.text.splice(op.position, 0, op.char);
    }
  }
}

Types of CRDTs

1. G-Set (Grow-only Set)

class GSet {
  constructor() {
    this.elements = new Set();
  }
  
  add(element) {
    this.elements.add(element);
  }
  
  merge(other) {
    for (const elem of other.elements) {
      this.elements.add(elem);
    }
  }
  
  has(element) {
    return this.elements.has(element);
  }
}

2. OR-Set (Observed-Remove Set)

class ORSet {
  constructor() {
    this.elements = new Map(); // value -> set of tags
    this.removed = new Set();  // removed tags
  }
  
  add(value) {
    const tag = generateUniqueId();
    const tags = this.elements.get(value) || new Set();
    tags.add(tag);
    this.elements.set(value, tags);
  }
  
  remove(value) {
    const tags = this.elements.get(value);
    if (tags) {
      for (const tag of tags) {
        this.removed.add(tag);
      }
    }
  }
  
  has(value) {
    const tags = this.elements.get(value);
    if (!tags) return false;
    
    // Has at least one non-removed tag
    for (const tag of tags) {
      if (!this.removed.has(tag)) return true;
    }
    return false;
  }
  
  merge(other) {
    // Merge elements
    for (const [value, tags] of other.elements) {
      const myTags = this.elements.get(value) || new Set();
      for (const tag of tags) {
        myTags.add(tag);
      }
      this.elements.set(value, myTags);
    }
    
    // Merge removed
    for (const tag of other.removed) {
      this.removed.add(tag);
    }
  }
}

3. LWW-Register (Last-Write-Wins)

class LWWRegister {
  constructor() {
    this.value = null;
    this.timestamp = 0;
    this.nodeId = null;
  }
  
  set(value, timestamp, nodeId) {
    if (timestamp > this.timestamp || 
        (timestamp === this.timestamp && nodeId > this.nodeId)) {
      this.value = value;
      this.timestamp = timestamp;
      this.nodeId = nodeId;
    }
  }
  
  merge(other) {
    this.set(other.value, other.timestamp, other.nodeId);
  }
}

Practical Implementation

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

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

// Shared types
const ytext = ydoc.getText('content');
const yarray = ydoc.getArray('items');
const ymap = ydoc.getMap('metadata');

// Connect to sync server
const provider = new WebsocketProvider(
  'wss://demo.yjs.dev', 
  'document-room', 
  ydoc
);

// Observe changes
ytext.observe(() => {
  console.log('Text changed:', ytext.toString());
});

// Make changes
ytext.insert(0, 'Hello World');
yarray.push(['item1', 'item2']);
ymap.set('author', 'John');

Collaborative Text Editing

import * as Y from 'yjs';

function CollaborativeEditor() {
  const [ydoc] = useState(() => new Y.Doc());
  const [text, setText] = useState('');
  
  useEffect(() => {
    const ytext = ydoc.getText('content');
    
    // Listen for remote changes
    ytext.observe(() => {
      setText(ytext.toString());
    });
    
    // Sync with server
    const provider = new WebsocketProvider(
      'wss://server.com',
      'doc-id',
      ydoc
    );
    
    return () => provider.destroy();
  }, [ydoc]);
  
  const handleChange = (e) => {
    const ytext = ydoc.getText('content');
    const newText = e.target.value;
    
    // Calculate diff and apply
    ytext.delete(0, ytext.length);
    ytext.insert(0, newText);
  };
  
  return <textarea value={text} onChange={handleChange} />;
}

Use Cases

1. Collaborative Editing

// Google Docs, Notion, Figma-style collaboration
// Multiple users edit same document simultaneously
// Changes merge automatically

2. Offline-First Apps

// Work offline, sync when online
// Conflicts resolve automatically
// No data loss

3. Distributed Databases

// Riak, Redis CRDT modules
// Eventual consistency
// High availability

Trade-offs

Pros

  • Automatic conflict resolution
  • No consensus required
  • High availability
  • Works offline

Cons

  • Increased memory usage
  • Complex implementations
  • Eventual consistency (not immediate)
  • Garbage collection challenges
Key Takeaway

CRDTs enable real-time collaboration without locks or consensus. They guarantee that all replicas eventually converge to the same state, making them perfect for collaborative editing, offline-first apps, and distributed systems. Use established libraries like Yjs rather than implementing from scratch.

Resources

Related Topics