Designing a Real-Time Collaborative Document Editor Like Google Docs

Problem Statement & System Requirements

Designing a cloud-based real-time collaborative document editor like Google Docs or Notion requires coordinating concurrent keystrokes across distributed clients while preserving deterministic convergence, sub-100ms propagation latency, and seamless offline reconciliation.

Functional Requirements

  1. Users can create, edit, and delete rich-text documents.
  2. Multiple users can edit the same document concurrently and observe real-time cursor positions and character mutations.
  3. The synchronization engine must resolve conflicting edits deterministically without losing user intent or corrupting document structure.
  4. Users can inspect immutable document revision history and restore previous checkpoints.
  5. Clients can edit documents offline using local persistence (IndexedDB) and reconcile queued deltas automatically upon reconnection.
  6. The system remains responsive on large documents experiencing hundreds of concurrent edits per minute.

Non-Functional Requirements

  1. Low Latency: Sub-100ms end-to-end propagation for active collaborators within the same region.
  2. Horizontal Scalability: Support millions of concurrent WebSocket sessions and billions of stored documents.
  3. Durability & Fault Tolerance: Zero committed operation loss during node failures via replicated write-ahead logs.
  4. Access Control Security: Enforce granular Role-Based Access Control (RBAC) across viewer, commenter, editor, and owner scopes.
  5. High Availability: 99.99% uptime backed by multi-AZ active-active gateways.
  6. Compact Version Storage: Combine fine-grained operation logs with periodic state snapshots to avoid unbounded storage amplification.

High-Level Distributed Architecture

A production collaborative editor separates connection termination, document coordination, and durable storage into specialized tiers:

  • Browser & Mobile Clients: Maintain persistent WebSockets connections to the edge gateway, apply local edits optimistically to the editor DOM, and buffer unacknowledged operations in an outgoing queue.
  • Consistent-Hashing Load Balancer: Routes WebSocket upgrade requests to collaboration shards based on document_id affinity so active collaborators on the same document converge on a single authoritative sequencer node whenever possible.
  • Collaboration Service Shards (ShareJS / Node.js): Execute Operational Transformation (OT) against incoming deltas, assign monotonically increasing revision numbers, and broadcast transformed operations.
  • Redis Pub/Sub Backplane: Connects multiple gateway instances subscribed to a given doc:{document_id} channel, ensuring sub-millisecond fan-out when collaborators connect through different edge pods.
  • MongoDB Document & Operation Store: Persists the append-only operations log, periodic compacted documents snapshots, and permissions ACL matrices.

Concurrency Control: Operational Transformation (OT) vs. CRDTs

Two mathematical models dominate collaborative text editing: Operational Transformation (OT) (pioneered by Jupiter and Google Wave/Docs, implemented in ShareJS / ShareDB) and Conflict-Free Replicated Data Types (CRDTs) (such as Yjs and Automerge).

Operational Transformation (OT) Mechanics

In a centralized OT architecture, every client tags its outgoing operation with the last acknowledged server revision $v$. If Client A and Client B both submit edits based on revision $v=10$:

  1. The server sequences Client A's operation $O_A$ first, advancing the document to $v=11$.
  2. When Client B's operation $O_B$ (still targeting $v=10$) arrives, the server runs transformation function $O'_B = \text{transform}(O_B, O_A)$ so that $O'_B$ accounts for the positional shift introduced by $O_A$, advancing the document to $v=12$.
export type TextOp =
  | { type: 'insert'; pos: number; text: string; clientId: string }
  | { type: 'delete'; pos: number; length: number; clientId: string };

/**
 * Transforms operation `opA` against a concurrently applied operation `opB`
 * so that applying `opB` followed by `transform(opA, opB)` converges.
 */
export function transformOp(opA: TextOp, opB: TextOp): TextOp {
  if (opA.type === 'insert' && opB.type === 'insert') {
    // Shift opA right if opB inserted before it, or break ties deterministically by clientId
    if (opB.pos < opA.pos || (opB.pos === opA.pos && opB.clientId < opA.clientId)) {
      return { ...opA, pos: opA.pos + opB.text.length };
    }
    return opA;
  }

  if (opA.type === 'insert' && opB.type === 'delete') {
    if (opB.pos + opB.length <= opA.pos) {
      return { ...opA, pos: opA.pos - opB.length };
    }
    if (opB.pos < opA.pos) {
      // Insertion occurred inside the deleted range; collapse to start of deletion
      return { ...opA, pos: opB.pos };
    }
    return opA;
  }

  if (opA.type === 'delete' && opB.type === 'insert') {
    if (opB.pos <= opA.pos) {
      return { ...opA, pos: opA.pos + opB.text.length };
    }
    return opA;
  }

  return opA;
}

Comparing OT and CRDTs (Automerge / Yjs)

Dimension Operational Transformation (OT / ShareJS) CRDTs (Automerge / Yjs)
Coordination Topology Requires a central authoritative sequencer per document to assign total revision order. Decentralized; any peer can merge state vectors commutatively without central sequencing.
Memory & Metadata Overhead Minimal; operations only store integer offsets and character payloads. Higher; every character or run requires unique Lamport timestamps and tombstone markers.
Offline Editing Supported by rebasing queued client operations against server history on reconnect. Native offline-first state synchronization with zero transformation replay cost.

Database Schema & Snapshot Compaction

Storing every keystroke indefinitely without compaction degrades document load times. A hybrid schema in MongoDB pairs periodic snapshots with bounded operation logs:

Collection Indexed Fields & Types Architectural Responsibility
documents _id (UUID), type (string), data (JSON delta), v (int64), updated_at (ISODate) Stores the latest compacted document snapshot and authoritative revision counter v.
operations document_id (UUID), v (int64), op (TextOp), user_id (UUID), ts (ISODate) Append-only write-ahead log with unique compound index { document_id: 1, v: 1 }.
permissions document_id (UUID), principals (array of { user_id, role }) Enforces RBAC checks during WebSocket handshake and before committing write operations.

Snapshot Compaction & Version History

While real-time collaborators exchange fine-grained delta changes, background compaction workers materialize a full snapshot every $N = 500$ operations (or after 5 minutes of idle time). This guarantees that opening a document requires fetching at most one snapshot plus at most 499 trailing operations.

Real-Time Synchronization & Offline Reconciliation

Clients maintain a three-state state machine modeled after the Jupiter OT protocol: Synchronized, AwaitingConfirm (one in-flight operation awaiting server ACK), and AwaitingWithBuffer (one in-flight operation plus locally buffered keystrokes). When a collaborator loses network connectivity:

  1. The client continues appending edits into its local IndexedDB buffer tagged with the last known server revision $v_{\text{base}}$.
  2. Upon re-establishing the WebSocket session, the client fetches server operations $(v_{\text{base}}, v_{\text{head}}]$ from the operations collection.
  3. The client transforms its buffered offline operations against the intervening server history and transmits the rebased batch for atomic commit and broadcast over Redis Pub/Sub.

Key References & Further Reading