The whole design, in writing
Learn system design by building a real-time collaborative editor like Google Docs step by step. An interactive guide covering the document model, conflict resolution with OT and CRDTs, op broadcast over WebSockets, persistence with an op log and snapshots, presence, offline sync, and access control.
Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.
The big idea
What is a collaborative editor?
Two people open the same document and type at the same time. Each sees their own edits instantly and the other’s appear live, and somehow both end up looking at the exact same text. No “file is locked”, no overwriting each other’s work.
Stop syncing whole documents and start syncing tiny operations — “insert ‘x’ at position 5”. A server orders everyone’s ops and resolves conflicts so every copy converges to the same state. Get that one idea right and the rest is plumbing.
What the new pieces do
- Editor Aclient
- One person’s browser. It applies edits locally for instant feedback, then sends them to the server — it can’t wait for a round-trip per keystroke.
Step 1 · The model
Edits are operations
If a save means “upload the whole document”, two people’s saves clobber each other and the last one wins. You can’t merge intent from snapshots — by then the information about what changed is gone.
Two people edit the same doc simultaneously. What does the client send on each change?
Whole-document saves clobber each other — last write wins — and the information about WHAT changed is gone, so you can’t merge intent. Concurrent editing needs the changes themselves.
Coarse diffs against a stale base still race and lose intent when positions shift, and they’re expensive to merge. You want the fine-grained edits as they happen.
Model the doc as an ordered sequence and stream small ops applied locally for instant feedback. Ops preserve intent and are cheap to send, order and merge — the op stream is the source of truth.
Model the doc as an ordered sequence and every edit as a small op (insert/delete at a position). The editor applies ops locally for instant feedback and streams them to an Edit Server over a persistent connection.
What the new pieces do
- Edit Serverbackend
- Holds a persistent connection to every editor of a doc. It receives edits, orders them, resolves conflicts, and relays the result to everyone.
- Document Modelservice
- The doc as a sequence of characters/objects with stable positions. Edits are tiny ops — insert at p, delete at p — not whole-document saves.
Step 2 · The hard part
Resolving concurrent edits
A and B both edit from the same starting text. A inserts at position 2; B deletes at position 1. By the time A’s op reaches B, B’s positions have shifted — apply A’s op blindly and the docs diverge. Forever.
A inserts at position 2; B deletes at position 1. By the time A’s op reaches B, positions have shifted. How do they stay identical?
Locking kills real-time collaboration — it’s back to "file is locked," the exact thing you’re replacing. Concurrent edits must be allowed and reconciled, not serialized.
OT rewrites an incoming op against ops it didn’t see (shift A’s "insert at 2" past B’s delete); CRDTs give each char a stable orderable id so edits merge commutatively. Both guarantee convergence — same ops, any order, same doc.
Applying ops blindly when their positions were computed against different states makes the documents diverge permanently. Concurrent ops must be transformed/merged, not applied as-is.
Use Operational Transformation (OT) or CRDTs. OT rewrites an incoming op against ops it didn’t see (shift A’s “insert at 2” to account for B’s delete). CRDTs give each character a stable, orderable ID so concurrent edits merge commutatively — no transform needed.
- convergentend state
- intentpreserved
- any ordersame result
What the new pieces do
- OT / CRDTservice
- The conflict-resolution core. It rewrites concurrent edits so that applying them in any order yields the same document on every screen.
Back of the envelope
- same ops, any order ⇒ same doc
- the convergence guarantee
- OT: transform vs unseen ops
- small ops, the server orders them
- CRDT: stable per-char ids
- commutative merge, no central transform
Step 3 · Make it live
Broadcast to everyone
A’s edit is resolved on the server — but B is still staring at stale text. Collaboration only feels real if the other person’s changes show up in well under a second.
A’s edit is resolved on the server, but B still sees stale text. How does B’s screen update live?
Polling is laggy and wasteful — collaboration needs sub-second updates, and most polls return nothing. The server should push the moment an op is accepted.
Re-downloading the doc is heavy, loses B’s cursor/scroll, and still lags. You want just the new ops pushed incrementally, not full reloads.
After accepting an op the server pushes it to every other editor’s open socket; when editors are on different servers, pub/sub relays it to whoever holds each connection. Real-time = push, not poll.
After accepting an op, the server broadcasts it to every other editor of the doc over their WebSockets. When editors sit on different servers, a pub/sub layer relays the op to whichever server holds each connection.
What the new pieces do
- Editor Bclient
- A second person editing the same document at the same time. The whole challenge is making A’s and B’s concurrent edits converge to one consistent doc.
- Op Broadcastbus
- Pushes each accepted op to all other editors of the doc in real time. Across servers, a pub/sub layer relays ops to whoever holds the other connections.
Step 4 · Never lose a keystroke
Persist: op log + snapshots
The live document lives in server memory. A crash, and hours of collaborative work vanish. But you also can’t rebuild a long doc by replaying millions of ops on every open.
The live doc is in server memory and a long doc has millions of ops. How do you avoid losing work AND loading fast?
The append-only op log is the truth (nothing lost on a crash); periodic snapshots let you load the latest checkpoint and replay only ops since — fast recovery, full history, free version control.
Rewriting the whole doc per keystroke is enormous write amplification and still loses concurrent intent. Append the small op, and materialize full state only occasionally.
Memory-only loses everything on a crash, and replaying millions of ops from scratch on every open is far too slow. You need durability (log) plus checkpoints (snapshots).
Append every accepted op to a durable Op Log, and periodically write a Snapshot of the full state. To load a doc, take the latest snapshot and replay only the ops since — fast recovery, full history, and free version control.
What the new pieces do
- Op Logstore
- An append-only history of every operation. Replaying it rebuilds the document and powers undo, version history, and catching up after disconnects.
- Snapshotsstore
- Periodic materialized copies of the doc so you don’t replay millions of ops from scratch — load the latest snapshot, then apply ops since.
Back of the envelope
- append op = the truth
- nothing lost on a crash
- snapshot = checkpoint of the log
- load latest + replay ops since
- log ⇒ undo, history, catch-up
- free version control
Step 5 · Who else is here?
Presence & cursors
Collaboration feels lonely — and people collide — if you can’t see where others are. Those colored cursors and name flags are core to the experience, but they’re high-frequency and disposable.
Colored cursors and "3 people viewing" update constantly and are disposable. How do you handle them?
Persisting a flood of high-frequency cursor updates would swamp the durable log with data nobody replays. Presence is throwaway — it shouldn’t touch document storage.
Mixing disposable cursor traffic into the durable op pipeline risks the document during a presence storm. Keep ephemeral signals on a separate path.
Track who’s connected and each cursor/selection, broadcast constantly, but never store it — if everyone leaves, presence evaporates while the doc remains. A cursor flood can’t risk the op log.
Track Presence separately from the document: who’s connected, and each person’s cursor/selection. Broadcast these constantly but never persist them — if everyone leaves, presence simply evaporates while the doc remains.
What the new pieces do
- Presenceservice
- Tracks who’s viewing, where their cursor and selection are, and broadcasts those colored carets — ephemeral state that never touches durable storage.
Step 6 · The flaky network
Offline & reconnection
Connections drop in tunnels and on planes. A user keeps typing offline, then reconnects — meanwhile others changed the doc. Their queued edits must merge cleanly, not overwrite or get lost.
A user edits offline in a tunnel, then reconnects — others changed the doc meanwhile. How do the edits merge?
On reconnect the client sends queued ops and pulls everything from the op log after its last-seen version; OT/CRDT merges the two streams. Offline editing is just real-time editing with a longer delay — same convergence machinery.
Overwriting discards everyone else’s changes made while the user was offline. Concurrent edits must merge, not clobber — exactly what OT/CRDT exists to do.
Forbidding offline edits breaks the experience on planes and in tunnels, and isn’t necessary — buffered ops plus version-based catch-up merge cleanly on reconnect.
Each client tracks the last version it saw. On reconnect it sends its buffered ops and pulls everything from the Op Log since that version; OT/CRDT merges the two streams. The same convergence machinery that powers live editing powers catch-up.
Back of the envelope
- client tracks last version N
- sync = "give me ops after N"
- buffered ops ⇄ pulled ops
- OT/CRDT merges both streams
- offline = real-time + delay
- one mechanism for both
Step 7 · Permissions & scale
The sharp edges
Not everyone may edit — some can only view or comment — and a viewer must not be able to sneak in an op. And a server can only hold so many live docs and connections before it tips over.
Check Permissions on join and on every op, so the server rejects edits from non-editors. Scale by sharding by document: all editors of one doc land on the same server (consistent hashing), keeping its in-memory state authoritative; pub/sub bridges across shards.
What the new pieces do
- Permissionsstore
- Who may open, comment on, or edit a doc. Checked when joining and on every op, so a viewer’s stray edit is rejected at the server.
You did it
You just designed Google Docs.
Everything you assembled, in order
- Edits are tiny operations on an ordered model, not whole-document saves.
- OT or CRDTs resolve concurrent edits so every copy converges.
- Accepted ops broadcast over WebSockets (and pub/sub) for live collaboration.
- A durable op log plus periodic snapshots give recovery, history and fast loads.
- Presence (cursors, who’s here) runs on a separate, ephemeral path.
- Versioned op logs make offline editing and reconnection sync just work.
- Server-side permissions and per-document sharding handle security and scale.