Blueprint
Realtime & messaging advanced

Design Google Docs

Two people type into the same sentence at once and both must end up with the same document — the whole interview is whether you can explain how, precisely.

asked at Google · Microsoft · Figma Sometimes asked ~40 min walkthrough ✎ Practice on canvas →
01

Scope the requirements

The heart of this question is one scenario: two cursors in the same paragraph, both typing, network latency between them. Everything else — WebSockets, storage, presence — is scaffolding around making that scenario converge. I'd name that scenario in the first minute so the interviewer knows I see where the difficulty lives.

Functional — what it must do

  • Multiple users edit the same document concurrently; everyone's edits appear for everyone in real time.
  • All collaborators converge to the same document state, whatever the interleaving of edits.
  • Presence: live cursors, selections, and who's-viewing indicators.
  • Documents are durable across sessions; sharing with viewer/editor permissions.
  • Explicitly cut for v1: full version history, offline editing, comments and suggestions — I'll sketch how each slots in if asked.

Non-functional — the numbers that shape the design

  • Edit propagation under ~100 ms same-region — collaborators should see keystrokes as typing, not as chunks.
  • Strong eventual consistency, deliberately not linearizability: every replica applies every op and all agree at quiescence.
  • Zero lost keystrokes once acknowledged — durability before ack on the op log.
  • Scale: millions of concurrent documents; each doc has few collaborators (2-100), docs up to ~10 MB of text.
  • ~10M concurrent WebSocket connections at peak across the fleet.
Say this out loud

The consistency requirement is the unusual one and I want to name it precisely: not linearizability — that would mean locking the document — but guaranteed convergence. Everyone ends at the same state without anyone ever being blocked from typing. That single requirement forces almost every design decision that follows.

02

Back-of-envelope estimates

The numbers reveal a comforting shape: per-document load is tiny — a handful of humans type slowly — so the challenge is fleet-wide connection count and op-log volume, not per-doc throughput. That's why one sequencer per doc works at all.

QuantityEstimateHow you got there
Concurrent connections~10M100M DAU, ~10% concurrent at peak. At ~100k WebSockets per host, that's ~100 connection hosts.
Ops per second, global~5M/sEach active editor emits ~1 op/s (keystrokes batched into small ops).
Op-log ingest~500 MB/s5M ops/s × ~100 B/op. Fleet-wide firehose; per-doc it's a trickle.
Per-doc op rate~2-100/sBounded by collaborator count and human typing speed — the reason a single sequencer per doc never becomes the bottleneck.
Document text~50 TB1B docs × ~50 KB average. Small. The op logs dominate storage many times over — snapshots plus truncation are mandatory, not optional.
Ops between snapshots~1,000Loading a doc = 1 snapshot + ≤1,000 ops to replay; older ops become prunable.
Say this out loud

Per document, load is human-bounded — nobody types faster than about ten ops a second. So I never need to scale a single document; I need to place millions of small, chatty documents across a fleet and keep each one's ordering authority on exactly one node.

03

Sketch the API

A thin REST surface for document lifecycle, and a WebSocket protocol where the actual editing lives. The load-bearing design choice is in the message schema: every op carries the version it was based on.

POST/v1/docs→ { docId }. Creates metadata, initial empty snapshot, owner ACL.
GET/v1/docs/{id}→ latest snapshot + ops since snapshot + current version. What a client loads before joining the live session.
WSwss://…/docs/{docId}The editing session. Client→server: { type: insert|delete, pos, chars, baseVersion }. Server→client: transformed ops from others, acks with assigned versions, cursor/presence messages.
PUT/v1/docs/{id}/permissionsGrant/revoke viewer or editor. Enforced at WebSocket connect and re-checked on revocation.
  • baseVersion on every op is the crux of the protocol: the client says "I made this edit against version 42", and the server knows exactly which concurrent ops (43, 44, …) to transform it against before applying. Without it, the server can't order edits correctly and convergence is unprovable.
  • The server assigns a monotonically increasing version per document on each accepted op — this total order is what operational transformation needs to be correct, and it's why exactly one server must own a given doc at a time.
04

Data model

The document is not a row that gets updated — it's an append-only log of operations plus periodic snapshots. That reframing is the data-model insight the interviewer is listening for.

documents
doc_id PK · owner · acl · latest_snapshot_ptr · latest_version
Metadata only, in Postgres — small, relational, transactional permission changes.
op_log
doc_id PK(partition) · version PK(cluster) · op · author_id · ts
Append-only, write-heavy, partitioned by doc_id — Cassandra or DynamoDB. Durability point: an op is acked only after this write.
snapshots
doc_id · version · blob (S3)
Materialised document every ~1,000 ops. Ops older than the newest snapshot are prunable.
presence
doc_id → { user_id, cursor_pos, ttl }
Redis only, short TTL, never persisted — cursors of people who left last Tuesday are noise, not data.
The database call

Postgres for metadata and ACLs because permissions want transactions; Cassandra for the op log because it's an append-only, doc-partitioned firehose at 500 MB/s that never needs cross-doc queries — exactly the workload Cassandra is shaped for; S3 for snapshots. If someone pushed me to simplify, I'd concede DynamoDB could hold both metadata and ops in one store — but I would not move the op log into Postgres, because 5M appends a second is not a relational problem.

05

High-level design

Editors hold WebSockets to a collaboration tier, and the routing rule is the architecture: every editor of a given document reaches the same server, which acts as that document's operational-transform engine and sequencer. Storage hangs off that server; compaction runs behind it.

client edge realtime data async Editor A Editor B WS gateway routes by docId Doc server OT engine + sequencer Doc placement consistent hash, etcd Postgres metadata, ACLs Op log Cassandra Presence Redis, TTL Compaction worker Snapshots S3, every 1k ops same doc → same node append, then ack truncate ACL at connect fold 1k ops op, baseVer lookup cursors
the invariant to notice: both editors' ops for one document funnel through one doc server — that single funnel is what makes operational transformation correct. presence goes to redis and never touches durable storage.
  1. Join: client fetches the latest snapshot plus the op tail over REST (ACL checked against Postgres), then opens a WebSocket; the gateway consults the placement map and pins the connection to the doc server that owns this docId.
  2. Alice types → her client sends { insert, pos: 5, "x", baseVersion: 42 }.
  3. The doc server transforms her op against any concurrent ops it accepted after version 42, assigns version 45, appends to the op log, and only then acks — durability before acknowledgement, zero lost keystrokes.
  4. The server broadcasts the transformed op to Bob and every other connected editor, who apply it locally; cursor positions ride the same channel into Redis with a short TTL.
  5. Behind the scenes, the compaction worker folds every ~1,000 ops into a new S3 snapshot and truncates the log — keeping doc loads fast and storage finite.
06

Deep dives — where the interview is won

Phase 01

Convergence: why last-write-wins fails, and what OT does

Start from the failure: the doc is cat, Alice inserts s at position 3 while Bob inserts black at position 0. If the server applies both literally, Bob's insert shifts the text and Alice's s lands mid-word — the replicas diverge or corrupt. Last-write-wins on the whole document is worse: someone's edit vanishes entirely. Operational transformation fixes this by adjusting each incoming op against the concurrent ops that were accepted before it: the server sees Alice's insert was based on version 42, Bob's op 43 inserted 6 chars before position 3, so Alice's position transforms from 3 to 9. Both orderings now converge to black cats.

The precondition — and the sentence interviewers wait for — is that OT needs a total order of operations per document, which is why exactly one server sequences each doc. Transformation without an agreed order is where a decade of buggy OT papers went to die. The client mirrors this: it applies its own edits optimistically, tags outgoing ops with baseVersion, and transforms incoming remote ops against its unacknowledged local ones, so typing never blocks on the network.

clients OT server A: ins @5 B: ins @5 Transform → @5, @6
concurrent edits at same position converge only if operations are transformed against each other.
Trade-offOT buys small payloads and small memory (the document is text plus a version counter) at the price of a central sequencer per doc — a coordination requirement CRDTs exist to remove.
Phase 02

OT vs CRDT: pick OT with a central server, and name what CRDTs buy

CRDTs make concurrent ops commute by construction: every character gets a globally unique, ordered position identifier (fractional indexing, RGA, as in Yjs or Automerge), so replicas can apply ops in any order and still converge — no sequencer, no server authority. That's genuinely better for peer-to-peer editing and long-offline merges. The costs are real, though: position identifiers and tombstones grow document memory well beyond the raw text, garbage-collecting them safely is subtle, and naive implementations interleave concurrently-typed runs of text mid-word.

My interview answer: since the requirements already include a server (permissions, durability, presence), I take server-ordered OT — smaller state, simpler correctness argument given the total order, and it's what Google Docs runs. I'd flag the escape hatch explicitly: if offline-first or P2P became a requirement, I'd reach for a mature CRDT library like Yjs rather than hand-rolling either algorithm — both have famously sharp edges.

clients sequencer A op B op Doc sequencer authoritative order b transformed a transformed
server orders ops. thin clients apply transformed ops. no peer-to-peer resolution.
Trade-offChoosing OT welds the design to per-doc server affinity — lose that (a network partition puts two sequencers on one doc) and correctness breaks, whereas a CRDT would merely lag; that's the risk I accept for the leaner state.
Phase 03

Doc affinity: consistent hashing on docId, and what happens when a server dies

All editors of a doc must reach the sequencer, so I hash docId onto a consistent-hash ring of doc servers, with membership and lease coordination in etcd or ZooKeeper. The gateway resolves docId to owner at connect time. Consistent hashing means a server joining or dying reshuffles only its own arc of documents, not the world. The failure sequence matters: the dead server's leases expire, its docs' arcs pass to neighbours, clients reconnect through the gateway to the new owner, which rebuilds state from snapshot plus op-log tail. Clients then resend unacknowledged ops with their baseVersions — the same machinery as normal editing, because an op is never lost once acked and never acked before it's in Cassandra.

The alternative worth naming: skip affinity, let any server accept ops, and serialise through Redis pub/sub or a per-doc Kafka partition. Simpler routing, but every op pays extra hops, and you've moved the sequencing problem rather than solved it. There's also a hot-doc wrinkle: a document with 5,000 viewers (a company all-hands doc) shouldn't hold 5,000 connections on the sequencer — read-only followers subscribe via pub/sub fan-out on other nodes; only editors talk to the sequencer.

docs servers snapshot doc-42 Server 1 owns doc-42 Server 2 S3 snapshot hash routes periodic save on failover
hash(docId) → one server. server dies → clients reconnect elsewhere, re-hydrate from snapshot + tail.
Trade-offAffinity gives single-node ordering with minimal hops but makes failover a real protocol (leases, fencing, replay) — the one thing that must never happen is two servers both believing they own a doc, so I'd fence with epoch numbers on op-log writes.
Phase 04

The op log grows forever unless snapshots truncate it

Storage math says text is ~50 TB but the op log ingests ~500 MB/s — about 40 TB a day. Unbounded, it also makes document loads unbearable: opening a year-old busy doc would replay millions of ops. The fix is classic log compaction: every ~1,000 ops per doc, a worker folds the log into a materialised snapshot in S3, updates the metadata pointer, and truncates ops older than the snapshot. A doc load is then one snapshot fetch plus at most a thousand small ops — tens of milliseconds.

Version history comes almost free from the same machinery: instead of deleting old snapshots, retain a sampled series of them (hourly, then daily, thinning with age) as the restore points users see. What you deliberately don't keep is every keystroke forever. And presence never enters any of this — cursors live in Redis with a TTL and die with the session; persisting them is a small mistake that reliably signals confusion about what's data and what's ephemera.

op log snapshot Ops 1..N Ops N+1.. Snapshot @N compress log truncated
every N ops or T seconds: snapshot doc state, prune log up to that op.
Trade-offAggressive truncation caps storage but coarsens history — you can restore Tuesday 3pm, not keystroke 4,081,223; retention granularity becomes a product decision you surface rather than an accident of the log.
07

Follow-ups you should expect

Why not lock the document, or sections of it, instead of all this transformation?
Locking is the pre-2005 answer and users hate it — two people genuinely do edit the same sentence in the same minute. Section locks fail at boundaries and turn every edit into lock negotiation. The entire product promise is lock-free concurrent editing, so convergence machinery isn't optional complexity; it is the feature.
How do cursors stay correct when other people's edits move the text?
A cursor is a position, so it's subject to the same transformation as ops: when a remote insert lands before your cursor, the client transforms the cursor position by the same rules. Same for selections — two positions. Candidates who get OT right often miss that cursors need it too, and the symptom is cursors visibly drifting mid-word during concurrent edits.
How do offline edits reconcile on reconnect?
The client buffers ops locally, each tagged with the baseVersion it saw. On reconnect it sends the batch; the server transforms them against everything accepted since — mechanically identical to normal editing with higher latency. For short offline windows OT handles this fine with no merge dialogs. For days-offline-then-merge as a first-class feature, I'd revisit the CRDT decision, and say so.
What happens when someone's edit access is revoked mid-session?
Permission checks at connect time aren't enough — the revoked editor holds an open WebSocket. The permission service publishes revocations to doc servers (pub/sub keyed by docId), and the doc server terminates the connection and rejects in-flight ops from that user. There's an unavoidable window of a second or so; the goal is bounding it, not pretending it's zero.
A doc goes viral inside a company — 5,000 people open it. What breaks?
Not op throughput — at most a handful are editing, and typing is human-bounded. What breaks is fan-out: broadcasting every op and cursor move to 5,000 connections from one node. Split roles: editors connect to the sequencer; viewers subscribe read-only via pub/sub across many fan-out nodes; and throttle presence updates for viewers — nobody needs 5,000 live cursors.

Where candidates lose the room

  • Proposing HTTP polling with whole-document saves — it turns concurrent editing into last-write-wins and fails the core requirement in one move.
  • Saying "use OT" or "use CRDTs" as a magic word without walking through one concrete two-edit transform — interviewers probe exactly here, and the hand-wave collapses.
  • Letting two servers accept ops for the same doc — OT's correctness rests on a per-doc total order; without affinity and fencing the whole argument is void.
  • No snapshot/compaction story — the op log is the dominant storage cost and unbounded replay makes old docs unopenable.
  • Persisting presence and cursors to the database — ephemeral data in durable storage signals you're not distinguishing state from signal.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.