Designing Google Docs: Real-Time Collaborative Editing with OT and CRDTs
Two users have the document "cat". User A inserts "s" at position 3 to make "cats". Simultaneously, user B deletes the character at position 1 to make "at". If you apply both operations naively in different orders on different replicas, the replicas diverge — one machine shows "ats", the other shows "ast" or worse. Every collaborative editor is, at its core, a machine for preventing exactly this.
This is one of the hardest system design interviews because the crux is not scaling — it is correctness under concurrency. Let us design it properly.
Requirements and the real constraint
Functional: multiple users edit the same document simultaneously, see each other's cursors, and every replica converges to the same content. Non-functional: keystroke-to-visible latency under ~100ms for a good feel, documents up to a few MB, sessions of 2-100 concurrent editors.
The real constraint: convergence. All replicas must reach identical state regardless of the order in which they received concurrent operations. Bandwidth and QPS are trivial by comparison — a fast typist emits ~8 ops/second of a few dozen bytes each.
Approach 1: Operational Transformation (OT)
OT is what Google Docs actually uses. The idea: when two operations happened concurrently, transform one against the other so the intent is preserved.
A: insert("s", pos=3) B: delete(pos=1)
B applies A's op unchanged: "at" -> insert at 3? transform!
transform(insert@3, delete@1) = insert@2 -> "ats"
A applies B's op unchanged: "cats" -> delete@1 -> "ats"
Both sides converge on "ats". The transform function T(op1, op2) must satisfy the TP1 property: applying op1 then T(op2, op1) equals applying op2 then T(op1, op2).
Writing correct transforms for insert/delete pairs is a weekend project. Writing them for rich text — formatting spans, tables, embedded objects — is years of edge cases. This is why almost everyone with a choice today picks a library (ShareDB) or a CRDT.
The architectural consequence of OT: transformation needs a total order of operations, so you need a central server per document that sequences ops. Clients send operations tagged with the last server revision they saw; the server transforms the incoming op against everything that landed since that revision, appends it to the log, and broadcasts.
// Server-side receive loop (simplified ShareDB model)
function receive(op: Op, clientRev: number) {
const concurrent = log.slice(clientRev); // ops client hasn't seen
for (const c of concurrent) op = transform(op, c);
log.push(op); // rev = log.length
broadcast(op, log.length);
}
Approach 2: CRDTs
A Conflict-free Replicated Data Type makes convergence a property of the data structure itself. For text, each character gets a globally unique, totally ordered ID (e.g., (counter, siteId) in RGA, or a fractional position list in Yjs). Insert means "place this character after ID X" — a statement that is unambiguous no matter when it arrives. Deletes mark tombstones.
Because operations commute, no central sequencer is required. Replicas can sync peer-to-peer, offline edits merge cleanly, and the server can be a dumb relay. The costs: metadata overhead per character (modern libraries like Yjs compress this aggressively), tombstone accumulation, and interleaving anomalies when two users type in the same spot.
OT vs CRDT: the decision table
| Dimension | OT | CRDT |
|---|---|---|
| Server | Required, sequences ops | Optional relay |
| Offline / P2P | Painful | Natural |
| Rich text correctness | Mature (Docs, Office) | Newer, now solid (Yjs, Automerge) |
| Per-char overhead | None | Metadata + tombstones |
| Implementation risk | Transform edge cases | Library does the hard part |
In an interview, say this out loud: OT centralizes the complexity in a transform function and demands a sequencing server; CRDTs move the complexity into the data structure and free the topology. Then pick one and go deep.
The rest of the system
Transport: WebSocket per client, sticky-routed to the server instance that owns the document session. Ownership via a directory service (document ID → session server) in Redis or ZooKeeper; if the owner dies, a new instance loads state and clients reconnect.
Persistence: append ops to a log (the source of truth), snapshot the materialized document every N ops so loading is snapshot + tail replay, not a million-op replay. Version history falls out of the op log for free.
Presence (cursors, selections): ephemeral state, broadcast on the same socket, never persisted. Cursor positions are transformed against incoming ops too — that is why remote cursors do not drift when you type above them.
Scale: co-editing sessions shard naturally by document ID. A single document rarely exceeds ~100 live editors; beyond that (a viral public doc), degrade gracefully to view-only followers reading from a fan-out of the op stream.
What interviewers probe
Why can't you just use last-write-wins? (You lose keystrokes — unacceptable.) Why does OT need a central server? (Transforms need a total order.) How does undo work? (Invert your own ops and transform the inverse against everything since — nastier than it sounds.) How do you handle a 10MB doc joining cold? (Snapshot + delta, lazy-load off-screen sections.)
The signal is not naming OT or CRDT — it is showing you understand why concurrent edits diverge and where each approach pays its complexity bill.
Keep reading
Designing Ad Click Aggregation: Exactly-Once Counting at Scale
Billions of clicks, billed to the cent: streaming aggregation with watermarks, dedupe, idempotent sinks, and lambda-style reconciliation.
Designing a CDN: Cache Hierarchy, Invalidation, and Request Routing
How a CDN actually works: edge PoPs, origin shields, consistent-hash cache keys, purge fan-out, and the anycast vs DNS routing decision.
Designing a Distributed Cache Service: Redis Cluster Internals and Hot Keys
Build the cache, not just use it: slot-based sharding, gossip and failover, eviction under memory pressure, and the hot-key problem that shards can't solve.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.