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
- Users can create, edit, and delete rich-text documents.
- Multiple users can edit the same document concurrently and observe real-time cursor positions and character mutations.
- The synchronization engine must resolve conflicting edits deterministically without losing user intent or corrupting document structure.
- Users can inspect immutable document revision history and restore previous checkpoints.
- Clients can edit documents offline using local persistence (IndexedDB) and reconcile queued deltas automatically upon reconnection.
- The system remains responsive on large documents experiencing hundreds of concurrent edits per minute.
Non-Functional Requirements
- Low Latency: Sub-100ms end-to-end propagation for active collaborators within the same region.
- Horizontal Scalability: Support millions of concurrent WebSocket sessions and billions of stored documents.
- Durability & Fault Tolerance: Zero committed operation loss during node failures via replicated write-ahead logs.
- Access Control Security: Enforce granular Role-Based Access Control (RBAC) across
viewer,commenter,editor, andownerscopes. - High Availability: 99.99% uptime backed by multi-AZ active-active gateways.
- 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_idaffinity 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
operationslog, periodic compacteddocumentssnapshots, andpermissionsACL 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$:
- The server sequences Client A's operation $O_A$ first, advancing the document to $v=11$.
- 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:
- The client continues appending edits into its local IndexedDB buffer tagged with the last known server revision $v_{\text{base}}$.
- Upon re-establishing the WebSocket session, the client fetches server operations $(v_{\text{base}}, v_{\text{head}}]$ from the
operationscollection. - 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.
