Design Collaborative Editing (Google Docs)
Difficulty: Tier 3 Asked at: Google, Notion, Figma, senior loops Time budget: 45–60 min
Two people typing in the same document at the same time, and it just… works, converging to the same text
without clobbering each other. That magic is the whole question, and it’s genuinely hard: concurrent edits
to shared state with no lost updates and guaranteed convergence. The signature algorithms are Operational
Transformation (OT) and CRDTs, over a real-time sync layer. This is the deepest concurrency
problem in the repo.
Prerequisites: Conflict Resolution / CRDTs, WebSockets, Design File Storage
1. Requirements
Functional:
- Multiple users edit the same document simultaneously; everyone sees everyone’s changes in near-real-time.
- Edits converge — all users end with identical content regardless of order.
- No lost updates; edits don’t clobber each other.
- Presence (who’s editing, cursors), edit history/undo.
- Offline edits sync on reconnect.
Non-functional:
- Low latency — keystrokes appear near-instantly for all editors.
- Convergence & consistency — the document is eventually identical for all (strong eventual
consistency).
- Scale to many docs, many concurrent editors per doc.
- Durable — never lose content.
Out of scope: rich rendering, the full app; focus on concurrent-edit convergence + sync.
2. The core problem: concurrent edits must converge
🚨 Two users edit “cat” at the same instant: user A inserts “s” at end → “cats”; user B inserts “H” at
start → “Hcat”. Both edits must apply on both sides and converge to “Hcats” — not “cats” on one screen and
“Hcat” on the other. The naive “last write wins” loses an edit. You need an algorithm that merges
concurrent operations correctly.
Two families solve this:
Represent edits as operations (insert@pos, delete@pos). When A’s op arrives at B, transform it
against the ops B already applied so positions still line up. A central server orders operations and
transforms them for each client. 🚨 Correct but complex — transformation functions are notoriously tricky.
Google Docs uses OT.
CRDTs (Conflict-free Replicated Data Types)
Structure the document so concurrent edits merge deterministically with no transformation — e.g. give
each character a unique, globally-ordered ID so inserts/deletes commute and always converge. 🚨 Simpler to
reason about, enables peer-to-peer/offline, but heavier metadata per character. Figma, Notion-style tools and
Yjs use CRDTs. (CRDTs)
3. High-level design
flowchart TB
A[User A] <-->|WebSocket: ops| Server[Collab Server<br/>orders + transforms ops]
B[User B] <-->|WebSocket: ops| Server
Server --> DocState[(Document state<br/>+ op log)]
Server --> Snap[(Snapshots + persistence)]
Server -->|broadcast transformed ops| A
Server -->|broadcast transformed ops| B
Clients send operations (not whole documents) over a persistent connection. The server (in OT) assigns a
canonical order, transforms each op against concurrent ones, applies it, and broadcasts to all clients, who
apply the transformed op to their local copy. Everyone converges. The op log + periodic snapshots give
durability and history.
4. Deep dives
4a. Why operations, not full-document syncs?
Sending the whole document on every keystroke is huge and race-prone (two full-doc writes clobber). Sending
small operations (insert ‘s’ at 3) is tiny, and — with OT/CRDT — mergeable. 🚨 Edits as composable
operations over shared state is the foundational idea; it’s what makes concurrent merge possible at all.
4b. OT vs CRDT — the central trade-off
| |
OT |
CRDT |
| How |
Transform ops against concurrent ops |
Ops commute by design (unique IDs) |
| Coordination |
Usually needs a central server to order |
Can be peer-to-peer / serverless |
| Complexity |
Transform functions are hard/subtle |
Simpler logic, heavier data model |
| Metadata |
Light |
Per-element IDs (can bloat) |
| Used by |
Google Docs, legacy |
Figma, Yjs, Automerge, newer tools |
🚨 State the trade-off and pick one with a reason: OT if a central server exists and you want compact data;
CRDT if you want offline/P2P and simpler convergence at the cost of metadata.
4c. Real-time sync & ordering
A persistent connection (WebSocket) streams ops both ways. The server (OT) is the ordering authority —
assigns each op a sequence number, ensuring a total order everyone can converge to. Clients apply their own
edits optimistically (instant local feedback), then reconcile with the server’s canonical order when acks
arrive. (WebSockets)
4d. Offline editing & reconnect
A user edits offline → accumulates local ops → on reconnect, sends them; the server transforms them against
everything that happened meanwhile and merges. CRDTs shine here (merge is inherent); OT needs careful
transformation of the backlog. Same sync-on-reconnect spirit as file storage/chat.
4e. Persistence, snapshots & history
Storing every op forever is expensive to replay. Periodically snapshot the document state and keep recent
ops since the snapshot → fast load (snapshot + replay recent ops), bounded storage. The op log powers undo/
redo and version history. Durable storage ensures no content loss.
4f. Scaling
Each document’s editing session is relatively contained (a doc has limited concurrent editors), so shard by
document — a doc’s collaboration is handled by one server/partition (which owns its op ordering). Millions
of docs spread across servers. A doc with thousands of simultaneous editors (rare) needs extra care
(hierarchical fan-out).
5. Bottlenecks & scaling further
- Concurrent-edit convergence → OT or CRDT over operations.
- Real-time latency → WebSocket ops + optimistic local apply.
- Ordering → central server (OT) assigns canonical order per doc.
- Offline/backlog → transform/merge on reconnect (CRDT easier).
- Storage/load → snapshots + recent-op replay; op log for history.
- Scale → shard by document; each doc’s ordering on one owner.
6. Trade-off summary
| Decision |
Chosen |
Alternative |
Why |
| Edit representation |
Operations |
Full-document sync |
Small, mergeable, race-safe |
| Convergence |
OT (or CRDT) |
Last-write-wins |
LWW loses concurrent edits |
| OT vs CRDT |
Per requirements |
— |
OT: central+compact; CRDT: offline/P2P+simple, heavier |
| Local UX |
Optimistic apply |
Wait for server |
Instant feedback; reconcile after |
| Persistence |
Snapshots + op log |
Replay all ops |
Fast load, bounded storage, history |
| Scale |
Shard by document |
Global |
Editing is per-doc; natural partition |
7. Follow-up questions
Why does last-write-wins fail for collaborative editing?
Because concurrent edits to a shared document are not competing writes of the same value — they're
independent changes that both need to survive, and last-write-wins discards one of them. If user A appends
"s" to "cat" and user B simultaneously prepends "H", the correct converged result is "Hcats," containing both
edits; last-write-wins would keep only whichever write arrived last, producing "cats" or "Hcat" and silently
losing the other person's keystroke. Worse, different clients might pick different "winners," so the document
diverges — people see different text, which is unacceptable. Collaborative editing therefore needs an
algorithm that *merges* concurrent operations so both are reflected and every client converges to the same
result, which is exactly what Operational Transformation and CRDTs provide. The problem isn't choosing a
winner; it's integrating all edits consistently, and LWW fundamentally can't do that.
Explain OT and CRDT and when you'd choose each.
Both guarantee that concurrent edits converge, but differently. Operational Transformation represents edits
as operations (insert/delete at a position) and, when an operation arrives that was made concurrently with
ones you've already applied, *transforms* it — adjusting its position so it still means the right thing given
the edits that happened in between — typically relying on a central server to impose a canonical order. It
yields compact data but the transformation functions are famously subtle and hard to get right. CRDTs instead
design the data model so operations *commute* by construction — for text, each character gets a unique,
globally-ordered identifier so inserts and deletes can be applied in any order and always converge with no
transformation needed — which makes the logic simpler and enables peer-to-peer and offline operation without
a central ordering authority, at the cost of carrying per-element metadata that can bloat the document. You'd
choose OT when you already have a central server and want minimal per-character overhead (Google Docs's
lineage); you'd choose a CRDT when you want offline-first or decentralized editing and prefer simpler,
provably-convergent merge logic, accepting the heavier metadata (Figma, Yjs, Automerge). Stating this trade-
off and picking with a reason is the crux of the answer.
Why send operations instead of syncing the whole document?
Because whole-document syncing is both wasteful and unmergeable. Sending the entire document on every
keystroke wastes enormous bandwidth for a one-character change, and more importantly, if two users each send
their full version of the document, there's no way to combine them — you'd have to pick one and clobber the
other's changes, reintroducing lost updates. Operations are tiny (insert this character at this position) and,
crucially, they're the unit that OT and CRDTs know how to *merge*: the algorithms transform or commute
individual operations so concurrent edits integrate correctly. Representing edits as small composable
operations over shared state is the foundational move that makes real-time collaboration possible at all —
it's what lets the system apply many people's changes incrementally and converge, rather than shipping and
fighting over monolithic snapshots. Snapshots still exist, but for persistence and fast loading, not as the
unit of live collaboration.
How do offline edits get merged when a user reconnects?
While offline, the client keeps applying edits locally and accumulates the corresponding operations in order.
On reconnect, it sends that backlog to the server, which integrates them against everything that happened in
the meantime. With CRDTs this is natural: the offline operations carry their unique identifiers and simply
merge with the concurrent operations, converging deterministically regardless of order — which is why CRDTs
are favored for offline-first tools. With OT, the server must transform the backlog of offline operations
against all the operations that were applied while the user was gone, adjusting positions so they still apply
correctly, which is doable but more delicate for a long divergence. In both cases the user's local view was
optimistically updated the whole time, so on reconnect their edits and everyone else's are reconciled into
the single converged document. It's the same sync-on-reconnect pattern as chat and file sync — accumulate a
delta offline, merge it on return — with the merge guaranteed correct by the convergence algorithm.
8. What junior / mid / senior answers look like
- Junior: syncs the whole document or uses last-write-wins — loses concurrent edits and diverges. May not
know OT/CRDT.
- Mid: represents edits as operations, knows OT or CRDT ensures convergence, uses WebSockets for
real-time sync and a central server for ordering.
- Senior: articulates the OT-vs-CRDT trade-off and picks with justification, uses optimistic local apply
with server reconciliation, handles offline backlogs via merge/transform, persists with snapshots + op log
for fast load and history, and shards by document — treating “concurrent edits converging without loss” as
the non-negotiable core.
Further reading