system-design

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:

Non-functional:

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:

Operational Transformation (OT)

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

  1. Concurrent-edit convergence → OT or CRDT over operations.
  2. Real-time latency → WebSocket ops + optimistic local apply.
  3. Ordering → central server (OT) assigns canonical order per doc.
  4. Offline/backlog → transform/merge on reconnect (CRDT easier).
  5. Storage/load → snapshots + recent-op replay; op log for history.
  6. 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


Further reading