Skip to main content

Design Google Docs

Premium

You're asked to design a collaborative editor like Google Docs. Two people can type into the same sentence at the same moment, and both must converge on the same text without losing either edit. Unlike chat, this system merges changes to shared state. Let's focus the design on conflict resolution rather than only real-time transport.

Clarify the requirements

  • How many concurrent editors? Assume tens of editors and hundreds of viewers per document rather than thousands. This keeps the per-document workload bounded.
  • What must converge? Text edits, cursors/selections, comments, formatting. Character-level merging applies to text; comments and formatting can ride simpler paths.
  • Offline? Yes: edit on a plane, merge on reconnect. This decision, more than any other, stresses the conflict model.
  • History? Version history with restore, and "who wrote this" attribution.
  • Latency feel? Local echo must be instant; remote edits should appear within ~100-200ms in-region.

Back-of-envelope numbers

Assume 100M active documents per day, with about 1% being edited concurrently.

  • Per document: an active editor produces a few operations per second, so 10 editors is roughly 50 ops/sec50\text{ ops/sec} on a hot document, which is tiny
  • Fleet-wide: 100M docs×1%≈1M100\text{M docs} \times 1\% \approx 1\text{M} live editing sessions
  • Write volume: at roughly 100 B per operation, a busy document writes about 5 KB/sec5\text{ KB/sec} to its log
  • Log growth: a year of edits on a heavy document reaches millions of operations, which makes snapshots a latency requirement rather than an optimization

The per-document load is small, while the fleet must hold many live sessions. We can spend our time on merging and session placement rather than raw operation throughput.

High-level architecture

Editors

① Gateways

② Doc sequencer
(one per live doc)

③ Op log

④ Snapshots

Components
  1. Gateways. The real-time pattern's connection tier, unchanged: sockets, heartbeats, dumb forwarding.
  2. Doc sequencer. The design's heart. Each live document is assigned (by consistent hashing) to one coordinator that receives ops, transforms them against concurrent ops, assigns each a sequence number, and broadcasts the result. One authority per doc makes ordering trivial.
  3. Op log. The append-only record of every transformed op, in sequence order. The source of truth; the document is this log.
  4. Snapshots. Periodic materializations of the log (say every 500 ops), so opening a doc is snapshot + tail replay, not a million-op replay.
Each document has one home: a sequencer that orders its ops. Everything else (log, snapshots, fan-out) hangs off that single point of truth.

Deep dive 1: why last-write-wins destroys work, and what OT does instead

Start with the failure. The doc says cat. Alice types s at position 3 (cats) while Bob types my at position 0 (my cat). Ship both edits as "replace the document" and one of them vanishes. Ship both as raw positional inserts and Bob's edit shifts Alice's target: applying insert('s', 3) after my landed gives my scat. Positions are relative to a document state, and concurrent edits invalidate each other's positions.

Operational transformation (OT) fixes the positions at merge time. The sequencer receives Alice's insert('s', 3), sees it was made against sequence 41 while Bob's op already became 42, and transforms it: Bob inserted 3 characters at position 0, so Alice's insert shifts to position 6, producing my cats. Every client applies ops in sequencer order, transforming their own in-flight ops the same way, and all replicas provably converge. The client types with zero latency (local echo), sends ops asynchronously, and reconciles when the transformed versions come back.

BobSequencerAliceBobSequencerAliceconcurrent with 42 → transform: 3 + 3 = 6both read "my cats"insert("my ", 0) @seq41op 42: insert("my ", 0)insert("s", 3) @seq41op 43: insert("s", 6)op 43: insert("s", 6)
The sequencer transforms Alice's stale-positioned op against Bob's concurrent one, and both edits survive.

The alternative family is CRDTs, where operations are designed to commute so replicas converge without a central sequencer. CRDTs shine for peer-to-peer and offline-first designs, and cost more in metadata and data-structure complexity. With Google Docs' shape (server-mediated, one home per doc), OT with a sequencer is the simpler choice. We should still compare both and explain why we're choosing one. And per the judgment call, let's identify where we need neither: cursors and presence are idempotent overwrites, and comments are append-only objects, so only the text stream pays the OT toll.

Deep dive 2: the op log, snapshots, and history

The operation log is the source of truth: an append-only, ordered list of operations, each stamped with a sequence number and author. We derive the current document by replaying that log from a snapshot.

This gives us three capabilities:

  • Fast open. Replaying a million operations on every open would be far too slow, so the system periodically stores a snapshot of the document state at a given sequence number. Opening a document means loading the most recent snapshot and replaying only the operations after it, so a document with a long history still opens from a few hundred operations.
  • Version history. Any past state of the document is just a snapshot plus a replay up to some sequence number. Attribution comes along for free, since every operation carries the ID of the user who made it.
  • Restore without rewriting history. Restoring an old version is implemented as a new operation, or batch of operations, that transforms the current state into the old one. The log stays append-only, so the audit trail is never edited.

Snapshotting runs as a background consumer of the log. Retention is a real design decision: a tenant under legal hold may need every operation kept indefinitely, while for everyone else you can coalesce fine-grained history older than 30 days into periodic checkpoints.

Deep dive 3: offline editing and rejoin

A user may edit for an hour on a plane, producing 2,000 local operations while the shared document advances by 500. On reconnect, the client submits its batch anchored to the last known sequence, and the sequencer transforms it across the intervening operations. We should bound that work. Beyond a threshold, show a three-way merge UI rather than silently merging weeks of divergent edits. Even a correct transform can miss the user's intent if a paragraph's meaning changed, so history and attribution remain the recovery tools.

Deep dive 4: what happens when the sequencer dies?

Assigning one sequencer per document makes ordering trivial, but that sequencer is also a single point of failure for the document. We need to decide what happens when it crashes.

How a document gets its sequencer. Documents are mapped to a pool of coordinator servers by consistent hashing on doc_id (routing and replication). A sequencer starts up when someone first opens the document and shuts down after the document has been idle for a while, so you're only paying for documents people are actually editing.

Recovering from a crash. Because the op log is durable and the sequencer holds only in-memory state, recovery is a matter of rebuilding that state. A replacement sequencer is assigned the document, reads the tail of the log, and reconstructs where the document left off. Meanwhile, clients that sent operations but never received an acknowledgement simply resend them. Each operation carries a (client_id, op_id) pair, so an operation that actually did make it into the log before the crash is recognized as a duplicate and dropped rather than applied twice.

Preventing two sequencers at once. The dangerous failure isn't a crash, it's a network partition, where the original sequencer is still running and still assigning sequence numbers while a replacement has been appointed. Two sequencers issuing numbers for the same document would corrupt the ordering the entire design depends on. The fix is fencing: each sequencer is issued an epoch number when it takes over, every write to the log carries that epoch, and the log rejects any write carrying an epoch older than the current one. The partitioned sequencer's writes fail, and it learns it has been replaced.

A document with many viewers. A company all-hands agenda might have 50 people editing and 5,000 watching. Keep editors connected to the sequencer, and serve viewers from a broadcast stream instead, which is the same fan-out inversion that Design Slack uses for very large channels.

Leveling signals

Mid-levelDraws the connection tier, explains why naive merging loses edits, and names OT or CRDTs as the two families of solution.
SeniorWalks through a concrete transform example, designs the sequencer with an op log and snapshots, and handles offline rejoin with bounded divergence. Keeps cursors and comments off the OT path.
Staff+Owns the sequencer's failure story with epoch fencing, replay, and idempotent resubmission. Sets a compaction and retention policy, splits hot-document viewers onto a broadcast path, and acknowledges that convergence guarantees consistency rather than intent.
Design Slack / TeamsHard

The messaging-shaped sibling, whose fan-out inversion applies to documents with many viewers.

Design Facebook MessengerHard

Per-user delivery state and reconnection, the parts a document editor handles differently.