Index/Google Docs

SponsorGitHub
Design16 min

Google Docs

Many people typing into one document at once, with no perceptible lag and no lost edits.

Primary concepts and the hard part

Concepts
Operational Transformation (OT)CRDTscausal consistencyWebSocketsevent sourcingoptimistic local applicationpresencesnapshotting
The hard part they’re probing
Convergence. Two users edit the same sentence simultaneously with no coordination. Both must end up with an identical document, neither edit may be silently lost, and neither user may see their own typing lag. Last-write-wins is catastrophically wrong here, and saying why is most of the answer.

Requirements

Functionalwhat it must do
Multiple users edit the same document concurrently
Every user's local edits apply instantly (no round-trip lag while typing)
All users converge to the same final document
Live cursors and selections of collaborators
Full revision history and restore
Offline editing that reconciles on reconnect
Non-functionalhow well it must do it
Local edit latency: 0ms (optimistic application)
Remote edit visibility: under ~200ms
Convergence guaranteed, never silent data loss
Documents up to ~100k characters with ~50 simultaneous editors
Out of scopedeliberately left out
Rich media embedding
Comment threads (a separate, simpler subsystem)
Permissions model details
Scale
Docs:            1B
Concurrent edit sessions: 1M
Ops per active editor:    ~5/sec while typing
Peak op rate:             a few M/sec across the fleet, but each doc
                          is independent → shard by doc_id
Op size:                  ~100B; a doc's op log can reach millions of entries
                          → SNAPSHOTS are mandatory
Conclusion
This is not a throughput problem — each document is an independent, small, low-traffic workload. It's a correctness under concurrency problem, and the architecture follows from that: one authoritative sequencer per document, and a convergence algorithm.

API / Model

WebSocket
WS
/v1/docs/{id}/connect
→ client sends: {type:"op", doc_id, base_version, op, client_id, seq}
← server sends: {type:"op", version, op, origin_client}
                {type:"ack", client_seq, version}
                {type:"presence", user_id, cursor, selection}
101Switching Protocols
REST
GET
/v1/docs/{id}
200OKlatest snapshot + version
GET
/v1/docs/{id}/history?from=
200OKop history
POST
/v1/docs/{id}/restore
{version}
200OK
primary or partition keysort keyforeign key → referenced columnnullableHover a table or column to trace its keys

High-level architecture

DatabaseCacheObject storageFocusClick a node for details

Each client edits its own copy immediately and syncs through the one Document Session Server that owns the document. WS Gateways carry ops in and out, and the operations log is the durable record everything else is rebuilt from.

  1. Client A applies the keystroke locally at once and keeps the op, with its base_version, in its pending buffer. The op travels over the WebSocket to the WS Gateway, which routes it by doc_id to the Document Session Server that owns the document, and the sequencer there assigns it the next monotonic version.
  2. If the op was based on an older version, the transform step adjusts it against the ops applied since, shifting its positions so it still lands where the user meant.
  3. The session server persists the op to the append-only operations log, keyed by doc_id and version.
  4. It broadcasts the transformed op through the WS Gateway, whose connection registry maps doc_id to connected clients, and acks Client A, which drops the op from its pending buffer.
  5. Client B transforms the incoming op against its own pending buffer and applies it locally.

Two background flows keep the log usable. Every N ops, a snapshot job writes the folded document to object storage, so a load reads the latest snapshot plus the ops after it. If the owner dies, a new owner is elected, rebuilds from snapshot and ops, and clients resend their unacked ops. Presence takes its own path: cursors and selections live in Redis with a TTL and reach collaborators through the gateway, never through the operations log.


Trade-offs and deep dives

01
Why last-write-wins is wrong here

If two users type into the same paragraph and you resolve by timestamp, one person's sentence silently vanishes while they're looking at it. In a document editor that's not an edge case, it's the normal flow. The requirement is that both edits survive and both users see the same result. That rules out every simple conflict-resolution strategy and is why OT and CRDTs exist.

02
Optimistic local application is non-negotiable

If a keystroke waits for a server round trip, typing feels broken at even 50ms of latency. So the client applies immediately, buffers the op as unacknowledged, and reconciles when the server responds. Everything else in the design exists to make that optimism safe.

03
Operational Transformation, in plain terms

Each operation carries the document version it was based on. When an op arrives based on a stale version, the server transforms it against every op that has been applied since, adjusting positions so the intent is preserved. An insert before your position shifts you right; a delete before your position shifts you left. The client performs the mirror-image transformation on incoming ops against its own pending buffer.

The honest caveat, worth volunteering: OT is notoriously difficult to implement correctly. The transformation functions must satisfy convergence properties (TP1/TP2) that are easy to get subtly wrong, and the number of cases grows with the richness of the operation set. Google Wave's OT implementation was famously hard. Acknowledging this is more credible than presenting OT as straightforward.

04
CRDTs, the alternative

Conflict-free Replicated Data Types assign each character a unique, densely-ordered identifier (a fractional index or a path in a tree), so operations are commutative by construction. Apply them in any order and you converge — no central sequencer, no transformation.

OTCRDT
Needs central serveryes (sequencer)no — works peer-to-peer
Metadata overheadlow (ops are small)high (per-character IDs; tombstones for deletes never fully go away)
Implementationcomplex transform matrixcomplex data structure, but composable
Used byGoogle Docs, EtherpadFigma, Automerge, Yjs, most newer tools

The recommendation to state: OT when a central server already exists and document size/memory matters; CRDT when you want offline-first, peer-to-peer, or simply want to avoid the transformation minefield. For a Google Docs clone with a server in the loop, either is defensible — what matters is that you can articulate the trade rather than declaring one universally better. Newer systems trend toward CRDTs because the tooling has matured.

05
One authoritative owner per document

Route all connections for a document to a single session server via consistent hashing on doc_id, with leader election so exactly one node owns it at a time. This gives a single point that assigns the total order. Without it, two servers could sequence conflicting ops independently and you'd need a much harder distributed agreement.

The failure question follows immediately: if that node dies, a new owner is elected and rebuilds state from the persisted op log (snapshot plus subsequent ops). Clients reconnect, send their unacked ops with base versions, and the new owner transforms and applies them. Brief unavailability for one document, and no data loss because the log is durable. Sharding by document means the blast radius is one document, not the platform.

06
Snapshotting is mandatory

A long-lived document accumulates millions of ops. Replaying all of them to open the file would take forever. Snapshot the materialized content every N ops (or on a timer), store it in object storage, and load as snapshot + ops since snapshot. This is standard event-sourcing practice, and forgetting it is a common gap.

07
Presence is deliberately lossy

Cursor positions update many times per second and are worthless a second later. Keep them in Redis with a TTL, broadcast at a throttled rate (a few times per second, not per keystroke), and never persist or order them. Treating presence with the same rigor as document ops would multiply your load for zero benefit — and saying so shows you're allocating engineering effort by value.

08
Offline editing

The client queues ops locally with their base version. On reconnect it sends the queue; the server transforms each against everything that happened meanwhile. This works well for short absences. For long ones the transformation chain gets expensive and the result can be semantically surprising even if technically convergent — at which point offering the user a "review changes" step is more honest than silent auto-merge. CRDTs handle this case more gracefully, which is a fair reason to prefer them if offline-first is a core requirement.

09
Undo is harder than it looks

Undo must be local — undoing your own last edit, not whoever edited most recently. That means maintaining a per-user undo stack whose entries are themselves transformed as other people's ops arrive, since the thing you want to undo may have moved. Worth raising as a known complexity rather than assuming it's free.

10
Version history

Falls out of the append-only log for free. Restoring a version is not a rewind — it's appending the ops that transform current state back to the target state, preserving history. Never mutate or truncate the log.


Possible follow-up questions

6 questions·try answering before you reveal