"Design Google Docs" is not a storage question. If your answer starts with buckets, chunking, and sync, you are answering a different problem — that one is covered in design Google Drive, which is about storing and syncing whole files. This page is about the harder and more interesting half: real-time collaborative editing, where several people type into the same paragraph at the same moment and every keystroke has to survive. Below is a structured walkthrough you can mirror in a real system design interview. Keep the system design interview cheat sheet open for the shared building blocks.
1. Clarify the requirements
Functional requirements
- Multiple users edit one document concurrently and see each other's changes within a few hundred milliseconds.
- No edit is silently lost, and all clients converge on the same final document.
- Live cursors and selections show who is where.
- Version history, undo/redo, and offline editing that reconciles on reconnect.
- Sharing with view / comment / edit permissions.
Non-functional requirements
- Low latency: local keystrokes must appear instantly, so edits apply optimistically on the client before the server confirms.
- Convergence: given the same set of operations, every replica must end up with identical text.
- Intention preservation: a merged result should still mean what each author intended.
- Durability: an accepted edit is never lost, even if a server dies a second later.
I'll assume documents in the tens-of-thousands-of-words range with up to ~50 simultaneous editors — enough concurrency to make the merge problem real without pretending we need a peer-to-peer mesh.
2. The core problem: why last-write-wins fails
The tempting design is to treat the document as a value in a database: whoever saves last wins. For text this is catastrophic. Two people editing different paragraphs both made valid, non-overlapping changes, but LWW keeps one version and throws the other away. Text is not a value being overwritten; it is a stream of tiny operations — insert a character at a position, delete a range — and merging has to happen at that granularity.
But naive index-based operations break too. Take the document "CAT" and two concurrent edits, each written against that same starting state:
Start: C A T (indices 0 1 2)
User A: insert("S", 3) intends -> "CATS"
User B: insert("H", 1) intends -> "CHAT"
Server applies B first, then A verbatim:
"CAT" --B--> "CHAT" (indices 0 1 2 3)
"CHAT" --A--> insert "S" at 3 -> "CHAST" WRONG
A's index 3 meant "after T", but B's insertion shifted
every index to the right of position 1 by one.
That is the whole problem in miniature: an operation's positions are only meaningful relative to the document state it was generated against. Any correct solution has to reconcile operations that were written against divergent states. There are two families of answers.
3. Approach A: Operational Transformation (OT)
OT keeps the intuitive representation — operations carry numeric positions — and fixes the mismatch with a transform function. Given two concurrent operations a and b generated from the same state, the transform produces a' and b' such that applying a then b' yields the same document as applying b then a'.
In our example, A's insert("S", 3) is transformed against B's insert("H", 1). Because B inserted one character at a position before 3, A's index shifts to 4:
transform( insert("S",3), insert("H",1) ) -> insert("S",4)
"CAT" --B--> "CHAT"
"CHAT" --A'--> insert "S" at 4 -> "CHATS" CORRECT
And symmetrically on the other client:
"CAT" --A--> "CATS"
"CATS" --B'--> insert "H" at 1 -> "CHATS" same result
Each client applies its own edit immediately, then sends it to the server tagged with the last server revision it has seen. The server holds the canonical operation log, transforms the incoming operation against every operation committed since that revision, appends the result, and broadcasts it. Clients that have unacknowledged local edits transform the incoming remote operation against their own pending queue before applying it. Google Docs is publicly documented as using an OT-based approach; the details above are a reasonable interview design rather than a description of Google's internals.
OT's strength is a compact document representation — the text is just text. Its weakness is that the transform functions are genuinely hard to get right: with rich text you need transforms for every pair of operation types, and correctness proofs for the general peer-to-peer case are notoriously subtle. Nearly every production OT system sidesteps this by routing everything through a central server that imposes a total order, which is a real architectural constraint worth stating out loud.
4. Approach B: CRDTs
A Conflict-free Replicated Data Type attacks the same problem by removing indices entirely. Every inserted character gets a globally unique, immutable identifier — typically a (site_id, counter) pair, plus either a position between two fractional keys or an explicit "insert after character X" reference. Ordering is then derived from the identifiers, with ties broken deterministically by site ID.
"CAT" as a sequence CRDT:
id: (s1,1) (s1,2) (s1,3)
ch: C A T
User A: insert "S" after (s1,3) -> id (sA,7)
User B: insert "H" after (s1,1) -> id (sB,4)
Neither references an index, so order of arrival is irrelevant:
C H A T S on every replica, always.
Deletes become tombstones: the character is marked removed
but its id stays so later operations still resolve.
Because concurrent operations commute by construction, no transform function is needed and no central authority is required — replicas can exchange operations peer-to-peer or in any order and still converge. The cost is metadata: every character carries an identifier, deleted characters usually persist as tombstones, and identifiers can grow as the document is edited, so real implementations need garbage collection and compaction. The document representation is heavier than plain text, and the algorithms, while easier to prove correct, are more intricate to implement efficiently.
| Dimension | OT | CRDT |
|---|---|---|
| Document representation | Plain text; light | Per-character IDs + tombstones; heavy |
| Topology | Practically needs a central server | Works peer-to-peer and offline-first |
| Correctness | Transform functions hard to prove | Convergence guaranteed by construction |
| Best fit | Server-mediated docs with rich text | Local-first apps, long offline periods |
5. High-level architecture
Client A Client B Client C
(editor + (editor + (editor +
pending queue) pending queue) pending queue)
| WebSocket | |
+---------+---------+--------+--------+
v
[ Document session server ]
- owns one doc's op log
- transforms / merges ops
- assigns revision numbers
- fans out to subscribers
|
+-----------------+-----------------+
v v v
[ Op log store ] [ Snapshot store ] [ Presence / pubsub ]
append-only materialized cursors, selections
revisions doc @ revision (ephemeral, TTL)
|
v
[ Metadata + ACL DB ]
owners, sharing, permissions
The key routing decision: all connections for a given document land on the same session server, chosen by consistent hashing on the document ID. That single-writer property is what makes ordering cheap. The server keeps the hot document in memory and flushes to durable storage; a load balancer plus a coordination service handles failover to a replica that replays the log.
6. Presence and cursors
Cursor positions, selections, and avatars are ephemeral — they should never touch the durable op log. Clients send lightweight presence updates over the same WebSocket; the session server fans them out and holds them in memory with a short TTL so a disconnected user's cursor disappears on its own. Remote cursor positions must be transformed alongside text operations, otherwise everyone's cursor drifts as edits land above them.
7. Persistence, snapshots, and undo
Operation log. Every accepted operation is appended with its revision number, author, and timestamp. This log is the source of truth and gives you version history and audit for free.
Snapshots. Replaying a million operations to open a document is unacceptable, so the service periodically materializes the document at revision R and stores it. Loading means fetching the latest snapshot plus the operations after it. Older log segments can then be compacted into named revisions for history.
Undo/redo in a shared document must be per-user, not global — pressing undo should reverse your last edit, not your colleague's. The client keeps a stack of its own operations, generates the inverse of the one being undone, and transforms that inverse against everything committed since. This is why undo is one of the fiddliest parts of a real editor and a good place to show depth.
8. Offline editing and access control
Offline. The client buffers its operations and the last revision it saw. On reconnect it replays the buffer to the server, which transforms each queued operation against the operations committed in the interim; the client then applies the operations it missed. With a CRDT the reconnect is a straight state merge. Either way, very long divergences are best bounded — past some threshold, prompt the user rather than attempting a heroic merge.
Access control. Permissions live in a separate metadata store: owner, per-user and per-group roles (view / comment / edit), and link-sharing settings. The session server checks the caller's role on connect and on every operation, and downgrades in real time — revoking edit access should close the write path immediately, not at the next page load.
Framework reminder: This answer follows the same arc as every system design response — requirements → the core problem (concurrent edits) → the key algorithmic decision (OT vs CRDT) → architecture → persistence → trade-offs. The routing and fan-out patterns here rhyme with the sharding you'd use in designing a distributed cache or a distributed key-value store.
Practice distributed systems with live AI support
CoPilot Interview surfaces a structured design skeleton — requirements, the concurrency problem, OT vs CRDT, persistence, and trade-offs — in about 4 seconds during real Zoom and Teams calls. Free tier for Windows and macOS. Explore the free AI interview assistant.
Download freeFAQ
Why does last-write-wins fail for collaborative text editing?
Last-write-wins treats the document as a single value and keeps only the newest version, so one user's edit silently erases the other's. Text editing is not a single value being overwritten - it is a stream of small insertions and deletions at positions, and two users typing in different paragraphs both made valid changes that should both survive. Any design that resolves concurrency by discarding a whole version loses real work, so collaborative editors merge at the level of individual operations instead.
What is Operational Transformation and how does it work?
Operational Transformation represents each edit as an operation such as insert(position, character) or delete(position). When two operations are created concurrently against the same document state, the server transforms each one against the other so that applying them in either order yields the same final document. If user A inserts at index 2 and user B concurrently inserts at index 8, B's operation is shifted to index 9 before it is applied after A's. A central server usually assigns the canonical order and transforms incoming operations against everything it has already accepted.
What is a CRDT and how does it differ from OT?
A CRDT, or Conflict-free Replicated Data Type, gives every character a unique immutable identifier and an ordering rule rather than a numeric index, so concurrent operations commute by construction and no transformation function is needed. Because merging depends only on the identifiers, replicas converge without a central authority, which makes CRDTs suitable for peer-to-peer and offline-first editing. The cost is metadata: every character carries an identifier, and deleted characters are often kept as tombstones, so the document representation is heavier than the plain text.
How do you persist a collaborative document?
The usual approach is an append-only operation log plus periodic snapshots. Every accepted operation is appended to the log with its revision number, which makes the document reconstructable and gives you version history for free. Because replaying millions of operations is slow, the service periodically writes a materialized snapshot of the document at a known revision, so loading a document means reading the latest snapshot and replaying only the operations after it.
How does offline editing get reconciled when a user reconnects?
The client buffers the operations it generated while offline along with the last revision number it had seen from the server. On reconnect it sends that buffer, and the server transforms each queued operation against every operation committed in the meantime before applying it, then sends the missed operations back so the client catches up. With a CRDT the reconnect is simpler because the two states can be merged directly, but very long offline periods still create large divergences that are best bounded by prompting the user to reload.