Design collaborative editing (Google Docs)
The interviewer asks
“Design a collaborative document editor like Google Docs, where many people edit the same document at the same time, see each other's cursors and changes instantly, can work offline briefly, and can see version history.”
What a strong answer looks like
The heart of this design is the concurrency algorithm, not the boxes. A strong answer explains why naive last-write-wins or locking fails, then picks Operational Transformation (OT) with a central server that orders operations per document (Google Docs' approach) or a CRDT (Yjs, Automerge) that merges without a central order. Around it: a document session service where all editors of one document connect to the same server (routing by doc_id), an append-only operation log as the source of truth, periodic snapshots so loading does not replay millions of ops, presence and cursors as ephemeral broadcast, and version history built from snapshots plus the op log. Scaling is mostly about many documents, each with few editors, so per-document ownership with consistent hashing works well.
Concepts to mention
1 Requirements
Functional
- Create, open and edit rich-text documents in the browser
- Real-time co-editing: changes from others appear within a fraction of a second
- All editors converge to the same document, with intent preserved (no lost typing)
- Live cursors and selections of other editors (presence)
- Comments and suggestions anchored to text ranges
- Version history: view and restore earlier versions; short offline editing that syncs later
- Sharing with view, comment and edit permissions
Non-functional
- Local edits apply instantly (0 ms); remote edits visible p95 under 200 ms in the same region
- Up to ~100 simultaneous editors and ~1,000 viewers on one document
- Never lose an acknowledged edit; durability of the op log is critical
- Document open p95 under 1 s for documents up to ~100 pages
- 100M documents active per month, ~5M concurrently open
Out of scope (say it out loud)
- Spreadsheets and slides (same principles, different data models)
- Full-text search across a user's documents (mention only)
- Exporting to PDF/Word
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
GET/v1/docs/{doc_id} | Load latest snapshot, revision number, and ops since snapshot; returns the session server address |
WS/v1/docs/{doc_id}/session | Real-time channel: send ops {base_revision, ops, client_id, seq}, receive acks and transformed remote ops, presence |
GET/v1/docs/{doc_id}/revisions?from=&to= | Version history list and diffs |
POST/v1/docs/{doc_id}/restore | Restore a version (implemented as a new op that sets content, not by deleting history) |
POST/v1/docs/{doc_id}/comments | Add a comment anchored to a range |
PUT/v1/docs/{doc_id}/permissions | Share with users or link, with role |
4 Data model
documentsSpanner / Postgres (sharded)doc_id (PK)owner_idtitlecurrent_revisionlatest_snapshot_revcreated_atupdated_atSmall metadata; current_revision is advanced only by the document's owning session server.
operations (op log)Bigtable / Cassandra / DynamoDBdoc_id (partition)revision (clustering)op payloadauthor_idclient_idclient_seqtsAppend-only, ordered per document; conditional write on revision guarantees no gaps or forks. Source of truth.
snapshotsObject storage (S3/GCS) + pointer in documentsdoc_idrevisionserialized documentCreated periodically; loading = snapshot + replay later ops.
permissions (ACL)Postgres / Zanzibar-style authz servicedoc_idprincipal (user/group/link)roleChecked on open and on each session connect; cached briefly in the session server.
commentsPostgrescomment_iddoc_idanchor (start/end positions tracked as ops apply)threadauthorresolvedAnchors are transformed along with ops so comments stay attached to text.
session routingConsistent hash ring / etcddoc_id → session serverAll editors of one doc meet on the same server.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Open a document
- 1Client calls GET /docs/{id}; the Document API checks permission and loads the latest snapshot and ops after it
- 2Client builds the document model and renders it; it now knows revision R
- 3Client opens a WebSocket; the router sends it to the session server that owns this doc_id (loading the doc into memory if it is not already active)
- 4Session server sends any ops after R and the current presence list (other users' cursors)
Type a sentence while someone else edits
- 1Each keystroke applies locally at once; the client batches ops for ~100 ms and sends {base_revision: R, ops, client_seq}
- 2Client keeps one batch in flight and buffers new local ops until the ack arrives (Google Docs-style client state machine)
- 3Session server receives the batch; if other ops were committed after R, it transforms the incoming ops against them
- 4It appends the transformed ops as revision R+k to the op log with a conditional write (only if current revision matches)
- 5It acks the sender and broadcasts the transformed ops to other connected editors
- 6Other clients transform the incoming op against their own pending local ops, apply it, and shift cursors and comment anchors
- 7All clients converge to the same text
Edit offline and reconnect
- 1Client keeps editing locally and queues ops against base revision R
- 2On reconnect it sends the queue; the server transforms them against everything committed since R
- 3Large divergence may produce surprising merges, so long offline sessions are limited or shown with a 'changes merged' notice
7 Deep dives
Why concurrent editing is hard: OT explained
Suppose the text is 'abc'. Alice inserts 'X' at position 0 and Bob deletes the character at position 2 ('c') at the same moment. If Bob's delete is applied after Alice's insert without adjustment, it deletes 'b' instead, because positions shifted. Operational Transformation fixes this: a function transform(op1, op2) adjusts op1 so it has the same intent after op2 has been applied, here moving Bob's delete to position 3. Generic peer-to-peer OT is notoriously hard to get right (it needs extra properties for correctness), so Google Docs uses a central server: every document has one authoritative sequence of revisions, clients send ops tagged with the revision they were based on, and the server transforms them against ops committed since then. Each client has at most one unacknowledged batch in flight and transforms incoming server ops against its local pending ops. This only needs the simpler transform property between two parties (client and server), which is much easier to implement and test. Rich text adds operation types for formatting (retain with attributes), and tables or lists need careful transform rules. The server-ordered model also gives a clean, linear revision history for free.
CRDTs as the alternative
A CRDT (Conflict-free Replicated Data Type) makes merging automatic: each character gets a unique, ordered ID (for example based on a logical clock and site ID), and inserts refer to neighbour IDs instead of numeric positions. Deletes mark characters as tombstones. Because operations commute, any replica can apply them in any order and still converge, which needs no central transform server and works well for offline-first and peer-to-peer apps. Yjs and Automerge are production libraries; Figma uses CRDT-inspired ideas for its object model. The costs: metadata overhead per character (much reduced in modern implementations, but still more than plain text), tombstones that grow unless garbage-collected, and interleaving anomalies when two people type at the same position concurrently. In an interview, choose one and justify it. OT with a central server fits a cloud product where everyone is online most of the time and you want one authoritative history. CRDTs fit offline-heavy, local-first or peer-to-peer products. Notably, even CRDT products usually still run a server for persistence, auth and fan-out, so the architecture diagram changes less than people expect.
One owner per document: session servers and routing
OT needs a single place that orders ops for a document, and the simplest way is to load each active document into one session server's memory. A routing layer maps doc_id to a server using consistent hashing with a coordination service (etcd or ZooKeeper) holding membership and leases; the load balancer or a thin router sends every WebSocket for that document to its owner. The owner holds the current document state and recent ops in memory, transforms incoming ops, and persists each revision to the op log with a conditional write on revision number. That conditional write is the safety net: if two servers ever believe they own the same document (split brain during a rebalance), only one can append revision N+1, and the loser must reload. Load is naturally spread because there are millions of small documents; the rare hot document (a company-wide doc open by 1,000 people) is still fine for one server because viewers only receive ops. When a server is drained or dies, its documents move to new owners that reload from snapshot plus op log, and clients reconnect and resend their pending ops, which are deduplicated by (client_id, client_seq).
Storage: op log, snapshots and version history
The op log is the source of truth: every committed revision is stored in order, keyed by (doc_id, revision), in a store that handles high-volume appends like Bigtable or Cassandra. Reading a document by replaying millions of ops would be slow, so snapshot workers consume committed ops and periodically write a full serialized document (every ~1,000 ops or 10 minutes of activity) to object storage, recording its revision. Opening a document then reads the latest snapshot plus fewer than ~1,000 ops. Version history is built on the same data: named versions are pointers to revisions, and the history UI groups ops by author and time into human-sized edits. Restoring a version is a new forward operation that sets content to that version, so history is never rewritten and collaborators simply receive it as another change. Old ops can be compacted: keep snapshots at daily granularity and drop fine-grained ops after a retention window to control storage. Comments and suggestions are anchored to positions that are transformed with every op, or to CRDT character IDs, so they stay attached to the right text.
8 Trade-offs
OT with central server vs CRDT
OT needs an authoritative server per document but gives compact ops and linear history, a natural fit for an always-online cloud editor. CRDTs allow any-order merging, offline and peer-to-peer at the cost of metadata, tombstones and some odd interleavings. Both are valid; justify based on offline needs.
Locking paragraphs vs real-time merging
Locks (only one person can edit a section) are simple but frustrating and break with offline. Real-time merging gives a much better experience, which is why modern editors use it.
Persist every op synchronously vs batch
Synchronous append before ack guarantees no lost edits but adds a few ms. Since local edits appear immediately anyway, that latency is invisible, so we persist before ack.
Presence through the same channel vs separate service
Cursor updates are frequent and ephemeral. Sending them through the session server is simple; they are throttled (e.g. 10/s) and never persisted. A separate presence service is only needed across documents (who is online in the drive).
9 Bottlenecks
A very popular document
1,000 viewers and 100 editors on one doc puts all fan-out on one session server. Split viewers onto read-only fan-out replicas that subscribe to the owner's op stream; only editors talk to the owner. Throttle cursor broadcasts.
Op log write rate
Millions of appends per second spread across documents partition well by doc_id. Client-side batching of keystrokes into 100 ms ops cuts writes by ~10x.
Large documents slow to open
Snapshot more often for big documents, lazy-load far-away pages, and store images and embeds separately.
10 Failure handling
Session server crash
Routing assigns the document to a new owner, which loads snapshot plus op log. Clients reconnect, send their pending ops with client_seq; already committed ones are recognised and acknowledged, others are transformed and applied. No acknowledged edit is lost because acks happen after the op log write.
Two servers think they own a document
The conditional append on revision number allows only one to commit N+1; the other gets a conflict, drops ownership and redirects clients.
Client bug sends a corrupting op
Server validates ops against the document schema before committing; version history allows restoring a good revision quickly.
11 What impresses
- Walking through a concrete insert/delete conflict example
- Explaining why Google chose server-ordered OT over peer-to-peer OT
- Comparing OT and CRDT honestly with when to use each
- Using conditional writes on revision to guard against split brain
- Showing how comments stay anchored as text changes
- Separating editors from viewers for hot documents
12 Mistakes
- Last-write-wins on the whole document
- Locking the document for one editor at a time
- Sending the full document on every change
- Letting multiple servers accept ops for the same document without ordering
- Persisting presence/cursor updates
- No snapshots, so opening a document replays its entire history
13 Follow-ups
Lessons this design uses
Practice questions
- Your multi-master store resolves conflicts with last-write-wins using server timestamps. Users report that their latest edits sometimes vanish. What is going on and what would you use instead?
- Two people edit the same paragraph of a shared document at the same moment. How do you make sure both end up seeing the same text, without losing either person's edit?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.