Back to Scenarios & Design Questions
Q10Question 10 of 67

How would you design a real-time collaborative document editor like Google Docs (OT vs CRDT)

Key Concepts
  • The core problem is merging concurrent edits so all replicas converge to the same final document (strong eventual consistency).
  • OT transforms operations against each other with a central ordering server; simpler data but complex transform logic.
  • CRDTs use stable unique character IDs instead of fragile positions, merging commutatively without a coordinator.
  • OT is simpler data and well-suited for server-coordinated editing; CRDTs are better for offline and P2P.
  • Persist an ordered operation log plus periodic snapshots for recovery, undo, and version history.
  • WebSockets provide low-latency bidirectional sync; edits are applied optimistically on the client.
  • Offline edits are buffered and replayed on reconnect; CRDTs handle this cleanly since ops commute.

Definition

The hard problem is merging concurrent edits so all replicas converge; OT transforms ops with a central server, CRDTs merge coordination-free, both backed by an op log, WebSocket sync, and offline replay.
  • Convergence – All replicas must reach the same document state despite edits arriving in different orders.
  • OT – Transforms each operation against concurrent ones so applying them in different orders yields the same result; needs a central server.
  • CRDT – Every character has a unique ID, not a position, so operations commute and merge without coordination.
  • Op log – The ordered log of operations is the authoritative history; periodic snapshots accelerate recovery.
  • Offline – Edits are buffered on the client; on reconnect, OT transforms them against missed ops, CRDTs simply merge.

Rajesh and Priya at National Bank of India both edit the same loan agreement. Rajesh inserts a word at the start while Priya deletes a word in the middle. If applied naively, positions clash and the document garbles. OT is a clerk who adjusts Priya's position to account for Rajesh's insert. CRDTs give every character a permanent ID so insertions and deletions never clash regardless of order.

Understanding the Concepts

What is the core problem?

Multiple users edit the same document concurrently. Edits are operations like "insert 'x' at position 5". Positions are relative to the document state, so concurrent edits applied in different orders produce different results. You need convergence: all replicas end identical.

JAVA
package com.bank.collab.model;

public class TextOperation {
    enum Type { INSERT, DELETE }
    private Type type;
    private int position;
    private String character;
    private String userId;

    public TextOperation(Type type, int position, String character, String userId) {
        this.type = type;
        this.position = position;
        this.character = character;
        this.userId = userId;
    }

    public Type getType() { return type; }
    public int getPosition() { return position; }
    public void setPosition(int p) { this.position = p; }
}

How does Operational Transformation work?

OT keeps operations position-based but transforms them against each other. If A inserted before B's target, B's position shifts.

JAVA
package com.bank.collab.ot;

import com.bank.collab.model.TextOperation;
import com.bank.collab.model.TextOperation.Type;

public class OperationalTransformer {

    public static TextOperation transform(TextOperation incoming, TextOperation applied) {
        TextOperation transformed = new TextOperation(
            incoming.getType(), incoming.getPosition(),
            incoming.getCharacter(), incoming.getUserId());

        if (applied.getType() == Type.INSERT) {
            if (incoming.getPosition() >= applied.getPosition()) {
                transformed.setPosition(incoming.getPosition() + 1);
            }
        } else if (applied.getType() == Type.DELETE) {
            if (incoming.getPosition() > applied.getPosition()) {
                transformed.setPosition(incoming.getPosition() - 1);
            }
        }
        return transformed;
    }

    public static void main(String[] args) {
        // Rajesh inserts 'A' at position 0
        TextOperation rajeshOp = new TextOperation(Type.INSERT, 0, "A", "Rajesh");
        // Priya deletes at position 5 (concurrent)
        TextOperation priyaOp = new TextOperation(Type.DELETE, 5, "", "Priya");

        // After applying Rajesh's insert, Priya's position must shift right by 1
        TextOperation transformedPriya = transform(priyaOp, rajeshOp);
        System.out.println("Priya's original position: " + priyaOp.getPosition());
        System.out.println("Priya's transformed position: " + transformedPriya.getPosition());
    }
}
TEXT
Priya's original position: 5
Priya's transformed position: 6

How do CRDTs work?

Each character gets a globally unique, ordered ID. The document is the set of identified characters sorted by their IDs. Operations commute by construction.

JAVA
package com.bank.collab.crdt;

import java.util.TreeMap;
import java.util.UUID;

public class SimpleCrdtDocument {

    private final TreeMap<String, Character> chars = new TreeMap<>();

    public void insert(String positionId, char c) {
        chars.put(positionId, c);
    }

    public void delete(String positionId) {
        chars.remove(positionId); // tombstone in real impl
    }

    public String render() {
        StringBuilder sb = new StringBuilder();
        chars.values().forEach(sb::append);
        return sb.toString();
    }

    public static void main(String[] args) {
        SimpleCrdtDocument doc = new SimpleCrdtDocument();
        // Rajesh inserts 'H' with ID "a1"
        doc.insert("a1", 'H');
        // Priya inserts 'i' with ID "a2" (after a1)
        doc.insert("a2", 'i');
        // Order of inserts does not matter: IDs determine position
        System.out.println(doc.render()); // "Hi"
    }
}
TEXT
Hi

What is the OT vs CRDT trade-off?

| Aspect | OT | CRDT |
|---|---|---|
| Data model | Plain text + positions | Characters with unique IDs |
| Central server | Required for ordering | Not required |
| Offline editing | Needs transform on reconnect | Merges cleanly (ops commute) |
| Metadata overhead | Low | Higher (ID per character + tombstones) |
| Complexity | Transform functions are hard to prove correct | Data structure design is complex |
| Used by | Classic Google Docs | Yjs, Automerge |

How do you persist the op log and snapshots?

The ordered operation log is the authoritative history. Periodic snapshots accelerate recovery.

JAVA
package com.bank.collab.persistence;

import java.util.List;

public class OpLogStore {

    public void appendOp(String docId, TextOperation op) {
        // Append to ordered log
    }

    public String loadDocument(String docId) {
        // Load latest snapshot
        String snapshot = loadSnapshot(docId);
        // Replay ops since snapshot
        List<TextOperation> ops = loadOpsSinceSnapshot(docId);
        return applyOps(snapshot, ops);
    }

    private String loadSnapshot(String d) { return ""; }
    private List<TextOperation> loadOpsSinceSnapshot(String d) { return List.of(); }
    private String applyOps(String s, List<TextOperation> ops) { return s; }
}

How does WebSocket sync work?

Edits are applied optimistically on the client for a snappy feel, sent over the socket, merged via OT or CRDT, and broadcast to all other clients.

JAVA
package com.bank.collab.sync;

public class CollabSyncFlow {
    // 1. Rajesh types a character -> applied locally immediately (optimistic)
    // 2. Operation sent to server via WebSocket
    // 3. Server applies OT/CRDT merge, assigns global order
    // 4. Merged result broadcast to Priya's WebSocket
    // 5. Priya's client applies the operation
    // Result: both see the same document
}

How does offline editing work?

Buffer local operations while disconnected. On reconnect, replay them.

JAVA
package com.bank.collab.offline;

import java.util.ArrayList;
import java.util.List;

public class OfflineBuffer {

    private final List<TextOperation> bufferedOps = new ArrayList<>();
    private boolean connected = true;

    public void onEdit(TextOperation op) {
        if (!connected) {
            bufferedOps.add(op);
        }
    }

    public List<TextOperation> onReconnect() {
        List<TextOperation> toReplay = new ArrayList<>(bufferedOps);
        bufferedOps.clear();
        connected = true;
        return toReplay;
        // OT: transform against missed ops
        // CRDT: simply merge (ops commute)
    }
}

What are common interview follow-up questions?

How would you implement undo in a collaborative editor? How do you handle rich text (bold, italic, links)? What is intention preservation? How do you garbage-collect tombstones in a CRDT? How would you add commenting and suggesting modes?

Visual Overview

DIAGRAM
┌──────────────────────────────────────────────────────────┐
│         COLLABORATIVE EDITOR ARCHITECTURE                 │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  Rajesh's Client              Priya's Client             │
│  ┌──────────────┐            ┌──────────────┐            │
│  │ Local edit   │            │ Local edit   │            │
│  │ (optimistic) │            │ (optimistic) │            │
│  └──────┬───────┘            └──────┬───────┘            │
│         │ WebSocket                 │ WebSocket          │
│         ▼                           ▼                    │
│  ┌─────────────────────────────────────────┐             │
│  │           Server (OT or CRDT)           │             │
│  │  1. Receive op                          │             │
│  │  2. Transform/merge against concurrent  │             │
│  │  3. Persist to op log                   │             │
│  │  4. Broadcast merged op to all clients  │             │
│  └───────────────────┬─────────────────────┘             │
│                      │                                   │
│              ┌───────▼───────┐                           │
│              │ Op Log +      │                           │
│              │ Snapshots     │                           │
│              │ (recovery,    │                           │
│              │  undo,        │                           │
│              │  version hx)  │                           │
│              └───────────────┘                           │
│                                                          │
│  Offline: buffer ops → replay on reconnect               │
│  Presence: ephemeral cursors via WebSocket               │
└──────────────────────────────────────────────────────────┘

Key Takeaway

The core problem in a collaborative editor is merging concurrent edits from many users so every replica converges to the same document. At National Bank of India, Rajesh and Priya co-edit a loan agreement, and if Rajesh inserts at the start while Priya deletes in the middle, naive position-based application garbles the document. OT transforms operations against each other with a central server that assigns order, while CRDTs give every character a unique ID so operations commute without coordination.

OT has simpler, smaller data but needs a central server and famously tricky transform logic. CRDTs are coordination-free and handle offline editing cleanly since ops commute, but carry heavier per-character metadata and tombstone overhead. Modern libraries like Yjs and Automerge favor CRDTs. Around either engine, Pankaj persists an ordered operation log as the authoritative history with periodic snapshots for fast recovery.

Live sync uses WebSockets: the client applies edits optimistically for a snappy feel, sends over the socket, the server merges, and broadcasts. Presence and cursors are ephemeral signals on the same socket. Offline edits are buffered and replayed on reconnect, where CRDTs merge cleanly and OT requires transforming against everything missed.

Remember one sentence: Merge concurrent edits with OT (transform against each other via a central server) or CRDTs (commutative structures that converge coordination-free), backed by a persisted op log, WebSocket optimistic sync, and offline buffering with replay.

---

Share
Use ← → arrow keys to navigate