Collaborative Editor
IntermediateOverview
A collaborative editor lets multiple users simultaneously view and edit the same document in real time, with every participant seeing changes within milliseconds. The central challenge is conflict resolution: when two users type at the same position at the same time, the system must converge to the same document state on every client without losing either user's work.
Functional Requirements
- Create, open, and edit text documents.
- Multiple users edit the same document concurrently in real time.
- All users converge to the same document state (no lost edits).
- Show each collaborator's cursor position and selection live.
- Persist documents durably with version history and undo support.
Non-Functional Requirements
- Low latency: local edits must appear instantly; remote edits should arrive within ~200 ms.
- Consistency: all clients must converge to the same document after all operations propagate (strong eventual consistency).
- Availability: the editor should remain usable during brief network interruptions (offline editing, sync on reconnect).
- Scale: support documents with up to 100 concurrent editors and millions of documents total.
Capacity Estimation
Assume 10M active documents/day, average 3 concurrent editors per document, and 5 operations/sec per user (keystrokes, selections).
- Concurrent connections: 10M x 3 = 30M WebSocket connections (spread across the day, peak ~5M concurrent).
- Operations/sec: 5M concurrent users x 5 ops/sec = 25M ops/sec at peak — this is the dominant load and is handled peer-to-peer per document session, not globally.
- Per-document session load: 3 users x 5 ops/sec = 15 ops/sec per document — easily handled by a single session server.
- Storage: a document ~ 50 KB; operation log ~ 10x document size over its lifetime. 10M docs x 500 KB = 5 TB/day of operation history, heavily compactable.
High-Level Architecture
Clients connect via WebSockets to a routing layer, which maps each doc_id to a single stateful Session Server. That session server is the central authority for that document, responsible for ordering operations, resolving conflicts (via OT or CRDTs), broadcasting changes to all other connected clients, and periodically snapshotting the document state to a durable database.
Data Model
| Entity | Fields / Schema | Storage Choice |
|---|---|---|
| document | doc_id (PK), title, owner_id, current_snapshot, version, created_at | Relational or document store (PostgreSQL/MongoDB) |
| operation | doc_id (partition), seq_id (sort), user_id, op_type, position, content, ts | Append-only log (Cassandra or DynamoDB) |
| snapshot | doc_id, version, content, created_at | Blob / document store (periodic compaction) |
| session | doc_id, node_id, active_users, created_at | In-memory registry (Redis) with TTL |
| presence | doc_id, user_id, cursor_pos, selection, color | In-memory on the Session Server, ephemeral |
Detailed Design
The Core Problem: Conflict Resolution
Two proven approaches exist to handle conflicts without locking the document:
- Operational Transformation (OT): The server acts as the single source of truth. It receives operations (e.g., "Insert 'A' at index 5"), transforms them if another operation happened concurrently, and broadcasts the transformed operation. This is what Google Docs uses. It requires a central server to dictate order.
- Conflict-Free Replicated Data Types (CRDTs): Data structures designed so that operations can be applied in any order and always converge. Clients can merge changes peer-to-peer. This is what Figma and modern editors often use. It trades server complexity for larger memory footprints and complex data structures.
Session Routing
Because OT requires a single source of truth, all users editing Document X must connect to the same server node. The API Gateway queries a Redis Session Registry to find which node 'owns' Document X. If none exists, it assigns one. If that node crashes, clients reconnect, the gateway assigns a new node, and the new node reconstructs the document state from the database.
Event Sourcing & Snapshots
The database stores the document as an event stream (every keystroke is an event). To prevent loading 100,000 events when opening a document, the server periodically compacts the events into a Snapshot (e.g., every 1,000 operations or every 5 minutes). When opening a document, the server loads the latest Snapshot and applies only the events that occurred after it.
Bottlenecks & Solutions
The single Session Server per document becomes a bottleneck if a document has thousands of concurrent viewers (e.g., a public announcement doc). To solve this, the architecture splits into Editors (routed to the main Session Server) and Viewers (routed to read-only replicas that receive broadcasted state updates via a pub/sub system like Redis PubSub).