Design problems
Messaging & collaborationHardSDE-3 40 min

Design collaborative editing (Google Docs)

Asked at
AdobeAtlassianGoogleMicrosoftSalesforceZoho

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

Operational TransformationCRDTs (Yjs, Automerge) as an alternativeCentral server ordering ops per documentOptimistic local apply with one in-flight batchSingle owner per document via consistent hashingAppend-only op log with conditional writesSnapshots plus op replayPresence and cursors as ephemeral dataIdempotent op resend with client sequence numbersVersion history and restore as a forward op

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

Concurrent open documents~5MMost have 1 editor; ~10% have 2+ concurrent editors
Operation rate~2-3M ops/s peak~10M active editors × ~1 op every 3-5 s after client-side batching of keystrokes (~100-200 ms windows)
Op log growth~10 TB/day~1M ops/s averaged over the day ≈ 86B ops/day × ~100 bytes; compacted into snapshots and old ops archived
Session servers~500-1,000Each holds ~10k open document sessions and ~50k connections in memory
Document size~100 KB-5 MBText plus formatting; images stored separately in object storage
Snapshot frequencyevery ~500-1,000 ops or 10 minKeeps load-time replay short: snapshot + < 1,000 ops

3 API

EndpointWhat 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}/sessionReal-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}/restoreRestore a version (implemented as a new op that sets content, not by deleting history)
POST/v1/docs/{doc_id}/commentsAdd a comment anchored to a range
PUT/v1/docs/{doc_id}/permissionsShare with users or link, with role

4 Data model

documentsSpanner / Postgres (sharded)
doc_id (PK)owner_idtitlecurrent_revisionlatest_snapshot_revcreated_atupdated_at

Small metadata; current_revision is advanced only by the document's owning session server.

operations (op log)Bigtable / Cassandra / DynamoDB
doc_id (partition)revision (clustering)op payloadauthor_idclient_idclient_seqts

Append-only, ordered per document; conditional write on revision guarantees no gaps or forks. Source of truth.

snapshotsObject storage (S3/GCS) + pointer in documents
doc_idrevisionserialized document

Created periodically; loading = snapshot + replay later ops.

permissions (ACL)Postgres / Zanzibar-style authz service
doc_idprincipal (user/group/link)role

Checked on open and on each session connect; cached briefly in the session server.

commentsPostgres
comment_iddoc_idanchor (start/end positions tracked as ops apply)threadauthorresolved

Anchors are transformed along with ops so comments stay attached to text.

session routingConsistent hash ring / etcd
doc_id → session server

All editors of one doc meet on the same server.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAHTTP + WebSocketopen docWS by doc_idwho owns doc?load snapshotappend rev N+1ack + remote opscommitted opsnew snapshotsnapshot revBrowser editorsLB + router (bydoc_id)Doc session servers(OT)Document APIPermissions serviceOp event streamSnapshot + indexworkersOp log (Bigtable)Routing (etcd ring)Snapshots (objectstore)Doc metadata DB
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Open a document

  1. 1Client calls GET /docs/{id}; the Document API checks permission and loads the latest snapshot and ops after it
  2. 2Client builds the document model and renders it; it now knows revision R
  3. 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)
  4. 4Session server sends any ops after R and the current presence list (other users' cursors)

Type a sentence while someone else edits

  1. 1Each keystroke applies locally at once; the client batches ops for ~100 ms and sends {base_revision: R, ops, client_seq}
  2. 2Client keeps one batch in flight and buffers new local ops until the ack arrives (Google Docs-style client state machine)
  3. 3Session server receives the batch; if other ops were committed after R, it transforms the incoming ops against them
  4. 4It appends the transformed ops as revision R+k to the op log with a conditional write (only if current revision matches)
  5. 5It acks the sender and broadcasts the transformed ops to other connected editors
  6. 6Other clients transform the incoming op against their own pending local ops, apply it, and shift cursors and comment anchors
  7. 7All clients converge to the same text

Edit offline and reconnect

  1. 1Client keeps editing locally and queues ops against base revision R
  2. 2On reconnect it sends the queue; the server transforms them against everything committed since R
  3. 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

Q1How do suggestions (track changes) work on top of the op model?
Q2How would you support offline editing for a day on a flight?
Q3How do you implement undo when others are editing too?
Q4How would you add an AI assistant that rewrites a paragraph while others edit?
Q5How do you enforce permissions if a user is removed mid-session?
Q6How would you search across all of a user's documents?

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.

Start free