“Design Google Docs” is the canonical real-time collaboration question. It looks like a document store, but the moment two people type in the same paragraph at the same moment, it becomes a distributed-systems problem: how do independent edits, applied in different orders on different machines, converge to the same document? The answer — Operational Transform or CRDTs — plus a low-latency sync layer, is what interviewers are really probing. Let’s build it.
Understanding the Problem
Functional Requirements
- Edit a shared document — Multiple users edit the same document simultaneously.
- Real-time sync — One user’s keystroke appears on everyone else’s screen within a fraction of a second.
- Conflict resolution — Concurrent edits to the same region merge without corrupting the document or losing work.
- Presence — Show who is in the document and where their cursor/selection is.
- Persistence & history — The document is saved durably and past versions can be restored.
- Offline editing — A user can keep editing offline; changes sync and merge on reconnect.
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Edit propagation (p99) | < 100ms between collaborators in a region |
| Concurrent editors/doc | Up to ~100 active editors on one document |
| Consistency | Strong convergence — all clients end at the identical document |
| Durability | No committed edit is ever lost |
| Availability | Edits continue through transient disconnects (queue & resync) |
| Scale | Hundreds of millions of documents |
Scale Estimation
- The system scales by number of documents, not a single giant one — hundreds of millions of docs, but each with a small, bounded set of concurrent editors (usually 1–10, occasionally ~100).
- An active editor emits maybe 1–5 small ops/sec while typing. A busy document is ~100 editors × a few ops/sec = a few hundred ops/sec — tiny per document, but multiplied across millions of live documents.
This shapes the architecture: each document is an independent, shardable unit with its own small op stream. There is no global state to coordinate — the hard problem is local to one document.
Core Entities and APIs
The key insight: don’t sync the whole document on every change. Sync tiny operations.
Data Model
Operation = {
doc_id: string
op_id: uuid
author: user_id
type: "insert" | "delete" | "format"
pos: int # position (OT) or element id (CRDT)
content: string # for inserts
rev: int64 # revision this op was based on
}
Document = {
doc_id: string
rev: int64 # current revision number
snapshot: blob # materialized text at some revision
# full state = snapshot + ops since that snapshot
}
Presence = {
doc_id: string
user_id: string
cursor: int # caret position / selection range
color: string
} # ephemeral, not persistedAPI Design
# Join a document (upgrades to a WebSocket)
GET /docs/{doc_id}/connect → 101 Switching Protocols (WebSocket)
on connect: server sends { snapshot, rev }
# Over the WebSocket (bidirectional messages)
→ { type: "op", op: {...}, base_rev } # client sends an edit
← { type: "op", op: {...}, rev } # server broadcasts others' edits
← { type: "ack", op_id, rev } # server confirms + assigns revision
↔ { type: "presence", cursor } # cursor / selection updates
# Snapshot / history (plain HTTP)
GET /docs/{doc_id}/history
POST /docs/{doc_id}/revert { to_rev }Representing edits as position-based ops (insert "x" at 42) rather than “here’s the new document” is what makes sub-100ms sync and conflict resolution possible.
High-Level Design
- Clients hold the current document plus a local op buffer. They apply their own edits immediately (optimistic UI) and send ops to the server.
- Sync service — Each document is a “room”. All editors of a document connect (via WebSocket) to the same room, handled by one authoritative process. The service orders incoming ops, transforms/merges them, assigns a revision number, and broadcasts them to every other editor in the room.
- Op log — An append-only, ordered log of operations per document is the source of truth. The document at any revision = a snapshot plus the ops after it.
- Document store — Periodic snapshots (the materialized text at a revision) are persisted so you don’t replay millions of ops to open a document.
- Presence service — Ephemeral cursor/selection state, broadcast to the room but not persisted.
A routing layer maps each doc_id to the server currently hosting its room (via a consistent-hash ring or a coordination service), so all of a document’s editors land on the same authoritative node.
Deep Dive: Conflict Resolution (OT vs CRDT)
This is the crux of the problem. Two users start from revision 5. Alice inserts “X” at position 2; Bob deletes the character at position 1 — at the same instant. Applied naively in different orders, their documents diverge. The fix is a merge algorithm that guarantees convergence: everyone ends identical regardless of order.
Operational Transform (OT)
OT transforms an operation against concurrent operations so it still has the intended effect. If Bob deleted position 1 before Alice’s “insert at 2” is applied, Alice’s op is transformed to “insert at 1” — its position is shifted to account for Bob’s edit. A central server imposes a global order and transforms each incoming op against the ops it hasn’t yet seen.
- This is Google Docs’ approach. It is efficient (ops stay small, no per-character metadata) and battle-tested.
- The cost: the transform functions are notoriously tricky to get correct for every op-pair, and OT essentially requires a central server to establish order — it doesn’t merge well peer-to-peer.
# Transform: adjust incoming op against an already-applied concurrent op
def transform(incoming, applied):
if incoming.type == "insert" and applied.type == "insert":
if applied.pos <= incoming.pos:
incoming.pos += len(applied.content) # shift right
# ... delete/insert, insert/delete, delete/delete cases ...
return incomingCRDTs (Conflict-free Replicated Data Types)
CRDTs design the operations so they commute — applying them in any order yields the same result — so no transform step is needed. A sequence CRDT (RGA, LSEQ, Yjs/Automerge) gives every inserted character a unique, immutable identifier with a fractional/logical position, so “insert after id Z” is unambiguous no matter what else happened.
- CRDTs merge without a central coordinator, which makes them excellent for offline and peer-to-peer editing — exactly the hard cases.
- The cost: each element carries metadata (ids, tombstones for deletes), so memory and payloads are larger, and garbage-collecting tombstones adds complexity.
Which to choose
State the trade-off explicitly: OT when you have a reliable central server and want minimal per-op overhead (classic Docs); CRDTs when offline-first or decentralized merging matters (Figma, modern local-first apps lean CRDT). Either satisfies the real requirement — strong eventual consistency: once all ops are delivered, every replica is identical.
Deep Dive: The Sync Layer and Offline
WebSocket rooms
Editing needs two-way, low-latency push, so the sync service uses WebSockets, not polling. All editors of a document share one room on one authoritative server. When a client sends an op, the server:
- Transforms/merges it against any concurrent ops (OT) or integrates it (CRDT).
- Appends it to the op log and assigns the next revision number.
- Acks the author and broadcasts the op to every other editor in the room.
Clients apply their own edits optimistically (instant local feedback) and reconcile when the server’s authoritative version arrives — so typing never feels laggy even over a slow link.
Offline and reconnect
Each client tracks the last server revision it has seen. While offline it queues local ops. On reconnect it sends its queued ops with their base revision; the server transforms them against everything that happened meanwhile and integrates them, then streams back the ops the client missed. Because the merge algorithm guarantees convergence, offline edits fold in cleanly — this is precisely where CRDTs shine and where OT needs careful base-revision bookkeeping.
Snapshots bound the cost
Replaying millions of ops to open an old document is infeasible, so the system writes a snapshot (materialized text at revision R) periodically. Opening a document loads the latest snapshot plus the handful of ops after it. Snapshots also power version history and “revert to” — each is a named point you can reconstruct.
Deep Dive: Persistence, Presence and Scaling
- Durability — The op log is written durably (and replicated) before an op is acked, so no confirmed edit is lost even if the room’s server crashes. On crash, another node loads the latest snapshot + log and resumes the room.
- Presence — Cursors and selections change constantly but don’t need persistence or the same ordering guarantees as edits. Broadcast them on a separate, best-effort channel (and throttle to ~10/sec) so presence traffic never competes with edits.
- Sharding by document — Because documents are independent, scale horizontally by assigning documents to servers (consistent hashing on
doc_id). A single document’s throughput is small and bounded, so one server comfortably hosts many rooms; only a pathologically popular document (thousands of editors) needs special handling — typically by making most participants read-only viewers served from a snapshot + broadcast, with a capped set of active editors. - Access control — Check permissions on connect (owner/editor/viewer); viewers receive broadcasts but their ops are rejected.
Key Takeaways
| Decision | Choice | Why |
|---|---|---|
| Unit of sync | Small operations, not whole documents | Enables sub-100ms updates and principled merging |
| Conflict resolution | Operational Transform or CRDT | Guarantees all replicas converge regardless of order |
| OT vs CRDT | OT = central, compact (Docs); CRDT = offline/P2P | Match to whether you have a reliable server |
| Transport | WebSocket rooms, one authoritative node/doc | Two-way low-latency push; a single ordering point |
| Source of truth | Append-only op log + periodic snapshots | Durability, history, and fast document open |
| Scale | Shard by doc_id (consistent hashing) |
Documents are independent; no global coordination |
| Presence | Separate best-effort channel | Cursor spam never blocks real edits |
A collaborative editor is a consistency problem in disguise. The interviewer wants to hear the words “Operational Transform” or “CRDT” and, more importantly, why they guarantee convergence; then a WebSocket sync layer with an op log, optimistic local application, snapshots, and sharding by document. Draw two users editing the same line and walk through how the merge keeps them identical — that is the moment the interview is won.
