Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design Google Docs

Step 1 / 9

Learn system design by building a real-time collaborative editor like Google Docs step by step.

The numbers to beatconvergentend stateintentpreservedany ordersame result

The whole design, in writing

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.

Editor Atypes
New in this step: Editor A.

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.

Edit ServerWebSocket hubDocument Modelordered units
New in this step: Edit Server, Document Model.

Two people edit the same doc simultaneously. What does the client send on each change?

  1. 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.

  2. 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.

  3. 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.

Why this piece earns its place

The knob this introduces is op granularity, and one op per keystroke is not the obvious answer it looks like. Coalescing a run of typed characters into a single op — flushed on a short timer, and immediately on a caret move, a paste, or a delete — cuts the message count enormously and still feels instant. Widen that window too far and the cost lands in three places at once: peers watch you type in lumps, the remote cursor drawn for you jumps instead of moving, and whatever is sitting unflushed is the only work in the system that nobody has written down yet. Make it too narrow and every editor emits a message per keypress, which everything downstream then multiplies. The other half of granularity is what each op carries. A delete has to record what it removed, not just where — otherwise you cannot invert it, and undo, along with anything else that walks the history backwards, stops being something the op stream can give you.

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.

Edit ServerWebSocket hubOT / CRDTtransform opsDocument Modelops + versions
New in this step: OT / CRDT.

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?

  1. 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.

  2. 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.

  3. 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.

Why this piece earns its place

The honest caveat is that OT’s correctness lives in a pile of transform functions, and their failure is silent. Plain inserts and deletes are the easy pairs; the ones that break are a bold span and a delete crossing it, a paste landing inside a table cell, two deletes that overlap. Get one pair subtly wrong and nothing throws — the two screens simply hold different text from that moment on, and somebody finds out when they reload. Two defenses. First, keep a single server as the ordering authority, which is what this design is already doing, so an op only ever has to be transformed against one linear history instead of against an arbitrary mesh of concurrent orderings; that second property is the genuinely hard one, and centralizing the order is how you avoid needing it. Second, don’t trust the transforms: have each client periodically send a cheap checksum of its text with the version it is at, compare server-side, and force a client that disagrees to resynchronize from the server’s copy. CRDTs swap this bill for another — deleted characters leave tombstones the structure has to keep carrying.

  • 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.

Editor BOp Broadcast
New in this step: Editor B, Op Broadcast. · swipe to pan the diagram

A’s edit is resolved on the server, but B still sees stale text. How does B’s screen update live?

  1. 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.

  2. 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.

  3. 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.

Why this piece earns its place

Broadcast is where the work stops being linear in what you type. One op becomes a message for every other editor of that doc, so a busy room costs the product of editors and edits, and the relay hop pays it again for every server holding one of those connections. Two things keep that affordable. Per connection, gather whatever ops are pending and write them as one frame on a short tick rather than a socket frame per op — the peer applies them in the same order either way, and that tick is latency you are choosing to spend, so keep it far under the sub-second promise. Then decide in advance what happens to a connection that cannot keep up: a phone on a bad network, a laptop that was suspended mid-session. The tempting answer is to buffer for it, and it is the wrong one, because unbounded per-connection queues turn one struggling client into server memory pressure that everybody in the document feels. Drop it, close the socket, and let it come back and catch up — nothing it missed is gone.

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.

OT / CRDTtransform opsDocument Modelops + versionsOp Logevery changeSnapshotsperiodic state
New in this step: Op Log, Snapshots.

The live doc is in server memory and a long doc has millions of ops. How do you avoid losing work AND loading fast?

  1. 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.

  2. 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.

  3. 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.

Why this piece earns its place

Two decisions hide inside “append it to the log”. The first is when you tell the client its op is safe. Acknowledge on receipt and you have opened a window in which the editor shows a character as saved that a crash would erase — precisely the thing this step exists to prevent — so the ack has to follow the durable write, not the receipt. Paying a disk round trip per keystroke would be miserable, so group them: ops arriving within a few milliseconds are written and acked together, which is one flush amortized across a whole room instead of one per keypress. The second is snapshot cadence, and it is a straight trade. Snapshot rarely and opening a busy document means replaying a long tail of ops while someone waits at a blank page. Snapshot constantly and you are back to rewriting the full document over and over, which is the write amplification you just rejected. Set it from replay cost rather than from a clock — checkpoint after a number of ops, build it off the serving path, and write it from a version you already hold rather than pausing the editors to take it.

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.

Edit ServerWebSocket hubDocument Modelops + versionsPresencecursors · who
New in this step: Presence.

Colored cursors and "3 people viewing" update constantly and are disposable. How do you handle them?

  1. 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.

  2. Mixing disposable cursor traffic into the durable op pipeline risks the document during a presence storm. Keep ephemeral signals on a separate path.

  3. 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.

Why this piece earns its place

Presence quietly assumes a connection tells you whether a person is there, and half-open connections are exactly where that stops being true. A lid closes, a tunnel swallows a socket, and the server still holds a connection with nobody on the far end — so the room keeps showing a cursor for someone who left, and it stays until something finally tears that connection down. Which means presence entries should expire on their own rather than waiting to be deleted: the client heartbeats, each entry carries a short lifetime, and anything that stops being refreshed disappears by itself. Key those entries per connection rather than per person, too, or the same user with the document open in two tabs overwrites their own cursor and it flickers between two places. The freeing part of this path is that it is the one stream you are allowed to lose. A cursor update is pure latest-value state, so the server can throttle it to roughly a frame rate and discard everything in between — doing that to an op would be data loss. Presence being disposable isn’t only a storage decision; it’s what lets you treat it as cheap when the system is under pressure.

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.

Editor AEditor BEdit ServerOT / CRDTDocument ModelOp LogSnapshotsPresence
New in this step: Editor B → Op Log. · swipe to pan the diagram

A user edits offline in a tunnel, then reconnects — others changed the doc meanwhile. How do the edits merge?

  1. 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.

  2. 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.

  3. 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.

Why this piece earns its place

Catch-up assumes the server can still produce every op after the client’s version, and that assumption expires. Op history isn’t retained at full resolution forever — that is what the snapshots are for — so a client that has been away long enough sits before the oldest op you still keep, and no amount of replay will reach it. Detect that case explicitly and hand it the latest snapshot plus the ops after that, rather than letting a request for ancient versions return an empty range that looks exactly like “nothing happened while you were gone”. The buffered side has its own edge. Those queued ops live in a browser tab, so they need writing to local storage as they are made, or the offline work survives the tunnel and not the closed lid. And bound the queue. Someone returning after days of solo editing is asking you to transform a very long buffer against an equally long stream, and what comes out of merging two documents that have drifted that far is rarely what either party wanted. Past that bound, stop calling it a merge: resynchronize from the server’s current state and keep their local work as a separate recoverable copy they can look at and decide about.

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.

Edit ServerDocument ModelSnapshotsPresencePermissions
New in this step: Permissions. · swipe to pan the diagram

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.

Why this piece earns its place

Pinning a document to one server buys simple ordering and pays for it with a handoff problem. That server will be redeployed, will lose its network, will pause long enough to look dead — and something has to let another server take the document over. Ownership as a lease from a coordination service is the usual shape, but the lease alone does not make it safe: the old owner can be paused past its expiry, wake up still believing it owns the doc, and append ops it ordered against a version the new owner has already moved past. The append is what has to be conditional. Every write to the Op Log carries the version it expects to extend and the ownership term it was made under, so a stale owner’s write is rejected by the log itself rather than trusted because a timer said so. The log arbitrates; the lease only decides who tries. Permissions have a smaller version of the same problem — a check on join is a check made once, and access gets revoked while the socket is still open. Re-check per op against state the server holds, and push the revocation to that live connection instead of waiting for the person to reconnect.

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.

Editor AEditor BEdit ServerOT / CRDTDocument ModelOp LogSnapshotsPresencePermissionsOp Broadcast
The finished design, end to end. · swipe to pan the diagram

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.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. OT vs CRDT — which would you choose and why?

    OT centralizes ordering on a server and keeps ops tiny, which is why Google Docs uses it — but the transform functions are notoriously hard to get right for rich content. CRDTs need no central transform and shine for peer-to-peer/offline-first (each character carries a stable id), at the cost of more per-character metadata and memory. Central server + rich text → OT; decentralized/offline-first → CRDT.

  2. How does undo work with concurrent editors?

    Undo must be selective: it reverts your own last op, not whoever edited most recently. You generate the inverse of your operation and transform it against everything since — so undoing your insert doesn’t clobber a collaborator’s later edit. The op log makes this possible because every change (and its author) is recorded in order.

  3. Why pin a whole document to one server (shard by doc)?

    It avoids distributed consensus on every keystroke. With all editors of a doc on one authoritative server, that server alone orders ops and holds the in-memory state — fast and simple; pub/sub bridges to editors connected elsewhere. Sharding by doc (consistent hashing) also spreads load and lets a hot doc’s server scale independently.

  4. How do comments and suggestions fit the op model?

    They’re ops too, but anchored to positions/ranges rather than mutating text. A comment references a span (via stable ids so it survives edits); "suggestion mode" edits are ops tagged as proposed — applied to a separate layer until accepted, when they become normal ops. Same op-log + convergence machinery, different op types.

  5. What’s the consistency model — is everyone always identical?

    Eventually consistent, converging fast. At any instant two editors may briefly differ (an op in flight), but OT/CRDT guarantees that once all ops are delivered every copy is identical — convergence, not lockstep. Applying your op locally immediately and reconciling when the server’s ordering arrives is what makes it feel instant.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Edits are sent as tiny operations (not whole-doc saves) because…

    Snapshots clobber each other and lose what changed; the op stream is the source of truth.

  2. OT and CRDTs both guarantee…

    They reconcile concurrent edits so every copy ends identical.

  3. Live updates reach other editors via…

    Real-time means push; pub/sub relays an op to whichever server holds each connection.

  4. Fast loads + durability come from…

    Log is the truth; snapshots are checkpoints — load latest, replay ops since.

  5. Offline edits merge on reconnect because the client…

    Versioned op logs make sync "ops after N, transformed against mine" — offline is delayed real-time.

Worked example: two concurrent edits to “hello” that end as the same text

The document reads hello at version 41. At the same moment Editor A types x at position 3 and Editor B deletes the e at position 1. Both ops are based on v41. Every string below was produced by running the ops; timings and snapshot cadence are illustrative.

StepWhereWhat happensNumbers
1Editor AApplies insert "x" @3 locally at once and sends it tagged “based on v41”, coalesced with any further typing on a short timerhelxlo on A’s screen; ~50 ms window (illustrative)
2Editor BApplies delete @1 locally; it reaches the Edit Server first and is ordered as v42hllo on B’s screen
3Edit Server → PermissionsA’s op arrives; A is an editor, checked on this op and not just at joinallowed
4OT / CRDTA’s op never saw v42. The delete sits before A’s position, so the insert shifts left by oneinsert "x" @2 = v43
5Document Model → Op Logv43 appended durably in a group commit with other ops from the same few milliseconds; only then is A acked1 flush shared by the room
6Op Broadcast → both editorsB receives v43 and inserts at 2. A receives v42; its pending insert at 3 is after position 1, so the delete applies unchangedB: hllo → hlxlo; A: helxlo → hlxlo
7PresenceA’s caret moves; the update is broadcast at roughly frame rate and never written to the log0 bytes persisted
8Clients → Edit ServerEach client sends a cheap checksum of its text at v43; they match, so no resyncconverged
9SnapshotsA checkpoint is written every 1,000 ops, off the serving pathopen = snapshot + under 1,000 ops replayed (illustrative)

Row 4 is the whole design in one line: without the shift, B would have shown hllxo while A shows hlxlo, forever (step 2 of the build). The latency a person feels is zero in row 1, because ops apply locally; what dominates the server is row 5’s durable write, which is why acks wait for a grouped flush rather than one disk round trip per keystroke (step 4). At 10× editors in one document, broadcast is what breaks first: every op fans out to every other editor, so the cost grows with the product of editors and edits; batch frames per connection and drop clients that cannot keep up (step 3). Row 8 exists because a wrong transform fails silently.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Co-edit: many people edit one doc at once and all see the same text.
  • Send ops: each change is a tiny insert/delete at a position, not a whole-doc save.
  • Broadcast: an accepted op appears on every other editor’s screen in well under a second.
  • Persist: never lose a keystroke, and still load a long doc fast.
  • Presence & permissions: show live cursors, and let only editors edit.

The qualities that shape everything

Each one names the mechanism that buys it.

Every copy ends identical
OT transforms an incoming op against ops it didn’t see, or CRDTs give each char a stable id — both guarantee convergence: same ops, any order, same doc.
Sub-second live updates
Push each accepted op over WebSockets to every other editor, with pub/sub relaying across servers — real-time is push, not poll.
Never lose work, still load fast
Append every op to a durable op log (the source of truth) and write periodic snapshots, so recovery loads the latest snapshot and replays only the ops since.
A cursor flood can’t risk the doc
Run presence — who’s here, each cursor/selection — on a separate ephemeral path that is broadcast constantly but never persisted to the op log.
Edit offline, merge cleanly on reconnect
Each client tracks its last-seen version; on reconnect it sends buffered ops and pulls ops since, and OT/CRDT merges the two streams — offline is delayed real-time.
Only editors edit, and it scales
Check permissions on join and on every op server-side, and shard by document so all editors of a doc share one authoritative server — no consensus per keystroke.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Tiny insert/delete ops over whole-document saves

Whole-doc saves clobber each other — last write wins — and throw away what changed, so intent can’t be merged. Small ops preserve intent, are cheap to order and merge, and the op stream becomes the source of truth.

OT / CRDTs over a global edit lock

A lock is just "file is locked" again — it kills real-time collaboration. Transforming or merging concurrent ops lets everyone type at once and still converge: same ops, any order, same document.

WebSocket push over polling for changes

Polling every second is laggy and mostly returns nothing; collaboration needs sub-second updates. The server pushes each accepted op the moment it lands, with pub/sub relaying across servers.

Op log + periodic snapshots over saving the whole doc per keystroke

Rewriting the full doc every keystroke is massive write amplification and still loses concurrent intent. An append-only log is the truth; snapshots checkpoint it so loads replay only the ops since.

Ephemeral presence path over cursor moves in the op log

Persisting a flood of high-frequency cursor updates would swamp the durable log with data nobody replays. Presence is throwaway — broadcast but never stored — so a cursor storm can’t risk the document.

The answer, out loud

What a strong answer to “Design Google Docs (Collaborative Editor)” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    What we’re actually building

    I want to be clear about the shape of this before I draw anything. Many people have the same document open, everyone types at once, everyone sees everyone else’s changes in well under a second, and when the typing stops every screen shows exactly the same text. No locking, nobody overwriting anybody. On top of that it can never lose a keystroke, it has to open a long document quickly, it should show live cursors, and only people with edit access get to edit. The hard requirement in there is the convergence one, so that’s where I’ll spend most of my time.

  2. 3–8 min

    Send edits, not documents

    The first decision is what a client actually sends, and I’d rule out the obvious one straight away. If a save means uploading the whole document, two people saving clobber each other and the last one wins — and worse, by then you’ve thrown away what changed, so there’s nothing left to merge. So I model the document as an ordered sequence and every edit as a small operation: insert this character at this position, delete that one. The editor applies its own op locally immediately, because you can’t wait for a server round trip between pressing a key and seeing the letter, and it streams the op to an edit server over a connection it keeps open.

    Built in step 1: Edits are operations
  3. 8–16 min

    Making concurrent edits agree

    Now the part that’s actually hard. Say we both start from the same text, I insert at position two, you delete at position one. By the time my op reaches you, your positions have moved, and if you apply mine literally our documents differ from then on, permanently. There are two families of answer. Operational transformation rewrites an incoming op against the ops it hasn’t seen, so my insert at two gets shifted to account for your delete. CRDTs instead give every character a stable id, so concurrent edits merge in any order with no transform at all. Both give the same guarantee — same ops, any order, same document. I’d take OT here, because there’s a server in the path ordering things anyway and the ops stay small.

    Built in step 2: Resolving concurrent edits
  4. 16–22 min

    Getting it onto the other screen

    That gets us correct, but it isn’t live yet — the server has resolved my edit and you’re still looking at old text. I wouldn’t poll for this. A poll is late by definition and nearly all of them come back with nothing. The server already holds an open socket to every editor of the document, so the moment it accepts an op it pushes that op down all of the others. Once there’s more than one server, two editors of the same document can be connected to different ones, so I’d put a pub/sub layer behind it that relays an accepted op to whichever server holds each connection.

    Built in step 3: Broadcast to everyone
  5. 22–29 min

    Where the document really lives

    So far the document only exists in server memory, and a crash costs everyone their afternoon. So every accepted op is appended to a durable log, and that log is the source of truth — the document is just what you get by applying it. You can’t open a document by replaying its entire history, though, so periodically I write a snapshot of the full state, and opening means loading the latest snapshot and replaying only the ops after it. The part I like is that the log I needed for durability also hands me undo, version history, and the way a disconnected client catches up.

    Built in step 4: Persist: op log + snapshots
  6. 29–34 min

    Cursors are a different kind of data

    Presence — the coloured cursors and who’s in the document — I’d deliberately run on its own path. It’s very high frequency and it’s disposable; nobody replays where a cursor was last Tuesday. So it gets broadcast constantly and never written to the op log. If everyone closes the tab, presence just evaporates and the document is completely untouched.

    Built in step 5: Presence & cursors
  7. 34–39 min

    Tunnels and planes

    Connections drop, and people keep typing anyway. The client already tracks the version it last saw, so reconnecting uses the same machinery as live editing: it sends the ops it buffered while it was away, pulls everything from the log after its version, and the transform merges the two streams. That’s worth saying out loud, because it means offline editing isn’t a separate feature with its own rules — it’s live editing with a longer delay.

    Built in step 6: Offline & reconnection
  8. 39–43 min

    Who may edit, and where it runs

    Two sharp edges left. Permissions: some people can only view or comment, and I don’t trust the client about that, so the server checks on join and again on every op and rejects edits from anyone who isn’t an editor. And scale: I’d shard by document with consistent hashing, so everybody editing one document lands on the same server and that server alone orders its ops. That’s the move that keeps me out of distributed consensus on every keystroke, and pub/sub is what bridges to the people connected elsewhere.

    Built in step 7: The sharp edges
  9. 43–45 min

    The trade-off, and what’s left

    The trade-off running through all of this is that everyone gets instant local feedback and the copies are only guaranteed identical once the ops have all landed. It’s convergence, not lockstep, and there really are moments where two people are looking at different text. I’ll take that, because the alternative is waiting on a round trip per keystroke. With more time I’d work through rich text, since transforming formatting spans is where OT gets genuinely difficult, and I’d go into comments and suggestions, which want to be anchored to a range that survives edits rather than to a position.

What this teaches

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.

Key takeaways

  • 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.

Concepts covered

  • What is a collaborative editor?
  • Edits are operations
  • Resolving concurrent edits
  • Broadcast to everyone
  • Persist: op log + snapshots
  • Presence & cursors
  • Offline & reconnection
  • The sharp edges
built to be co-edited, not memorized — make the calls, cut the broadcast, run the gauntlet.
Finished this one? 0 / 65 System Designs done
NextDesign a Notification System Closest system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs