Real-time collaboration
How several people edit one document at the same time: each person sees their own typing instantly, a server puts everyone's changes in one order, and every copy ends up the same.
Why editing together is hard
In Google Docs, Figma, or Notion you can watch a colleague type a few words away from you while your own typing never stops. It looks simple. It is one of the hardest features to build on the web.
There is no single document on screen: every person has their own copy in their own browser tab. Changes travel between the copies over the network, and the network is slow compared with typing. Typical round trips are roughly 30 to 80 ms on good office Wi-Fi, 150 to 400 ms on a phone, and seconds on a train. While a change is in flight, everyone else keeps typing.
A two-person story that goes wrong
Asha and Ben share a document whose only line is big cat. Count the letters from 0: b is 0, the space is 3, and c is 4. At the same moment, Asha inserts black at position 4, so her copy reads big black cat. Ben deletes the 4 characters from position 0, so his copy reads cat.
Each editor sends its change to the other as an instruction. Asha's says "insert black at 4". Ben's says "delete 4 characters at 0".
Ben's editor receives Asha's instruction, but his copy is only 3 characters long. Position 4 doesn't exist, so a lenient editor puts the text at the end: catblack . Asha's editor applies Ben's delete to big black cat and gets black cat.
Both editors did exactly what the instructions said. No error was thrown, and the two people now look at different documents without knowing it. Every later change will land in the wrong place.
The root cause: each instruction named a position measured on the text its author saw. The other person's edit moved the text underneath it. Any edit that says "at position N" can be broken by an edit its author didn't know about, and in a live editor, that happens many times a minute.
The three guarantees a collaborative editor must keep
Research on group editors in the 1990s named the properties a correct system needs. You will hear these words in interviews.
- Convergence. Once everyone has received every change, all copies are identical. The story above breaks this.
- Intention preservation. Each change does what its author meant. Asha meant "put
blackin front ofcat". The right final text isblack cat, with her word kept and Ben's deletion kept too. - Causality. If one change was made after seeing another, nobody sees them in the opposite order. If Ben answers Asha's comment, no one should see his answer before her comment.
Why not just lock?
The obvious fix is a lock: while Ben edits a paragraph, nobody else may. Some products do lock big objects, like a spreadsheet cell while someone types in it, or a slide in a presentation tool. For flowing text, locks fail in three ways.
- People wait. Getting the lock costs a round trip before the first letter appears.
- Locks get stuck. A laptop that sleeps holding a lock blocks everyone until a timeout.
- Users don't expect them. Since Google Docs, everyone expects to type freely in the same paragraph.
So modern editors don't prevent concurrent edits. They let them happen and merge them. This lesson covers the architecture every such editor shares: local echo, a server that orders changes, presence kept apart from content, and snapshots with history. The merge algorithms get their own lessons: operational transformation (OT), which rewrites the positions in incoming edits, and CRDTs (conflict-free replicated data types), which give every character or object a stable id.
Check your understanding · Exercise 1
Which of these pairs of edits are concurrent? Select all that apply.
The mental model: copies, a referee, and a log
Picture five people reviewing a printed report in a meeting room. Everyone scribbles on their own copy right away, so nobody waits. One note-taker keeps the official copy.
People call out their changes, the note-taker writes each one down in the order heard, and reads it back to the room. When you hear a change that shifts the line you were working on, you fix your own copy to match.
The scribbling is local echo: you see your own change at once. The note-taker is the server, which decides one official order. The note-taker's list is the operation log. Fixing your copy to fit someone else's change is the merge, done by operational transformation or a CRDT.
Send changes, not whole documents
The first design choice hides in the word "change". A simple app saves by sending the whole document: PUT /docs/42 with 80 KB of text. If two people do that, the last request wins and the other person's work is gone. This is last writer wins, and for a shared document it means lost work.
Collaborative editors send operations instead: small descriptions of one change, like "insert black at 4" or "set the fill of shape 17 to blue". An operation is usually 20 to 200 bytes. Because each one describes only what its author touched, two operations on different parts of the document never overwrite each other, and two on the same part can be merged by looking at both.
// A text editor: positions in a string
type TextOp =
| { kind: "insert"; pos: number; text: string }
| { kind: "delete"; pos: number; length: number };
// A design tool: properties on objects with ids
type PropOp = { kind: "set"; objectId: string; key: string; value: unknown };
// What travels over the wire
type Envelope = {
clientId: string; // which tab sent it
seq: number; // that tab's own counter, for de-duplication
baseRev: number; // the server revision the author was looking at
ops: TextOp[] | PropOp[];
};
The document is the result of its log
Once changes are operations, the document stops being a thing you store and becomes a thing you compute. Start from an empty document, apply operation 1, then 2, then 3, and so on. If two copies start from the same state and apply the same operations in the same order, they must end up identical. That one sentence is how a central server gives convergence.
This also explains the other features. Version history is the log read at an earlier point. Undo is a new operation that reverses an old one.
A snapshot is a saved result of replaying the log up to some point, so you don't have to replay from the start every time. Each later chapter builds one of these on top of the log.
Two loops run at once
Inside each browser tab, two loops run side by side, and keeping them apart in your head makes the rest of the lesson easy.
- The local loop, every keystroke: read the input, build an operation, apply it to the local copy, re-render. It never touches the network and must finish in a few milliseconds.
- The sync loop, in the background: send pending operations to the server, receive confirmations and other people's operations, merge them into the local copy. It runs at network speed, tens to hundreds of milliseconds.
Bugs almost always come from where the two loops meet: an operation arrives while the user has local changes it doesn't know about. The next two chapters cover that meeting point from the client's side and the server's.
Check your understanding · Exercise 2
Why do collaborative editors send operations instead of the whole document? Select all that apply.
Local echo: your own edits never wait
Typing has to feel instant. Past about 100 ms, people notice the gap between pressing a key and seeing the letter, and good editors aim for one frame, about 16 ms.
A round trip to the server alone is 30 ms on a good day and 300 ms or more on a phone. So a collaborative editor can never wait for the server before showing a keystroke. It applies the operation to the local copy at once, paints, and only then sends it. This is local echo, sometimes called local-first echo: your screen shows the result before anyone else has agreed to it.
This is an optimistic update (showing a change before the server confirms it) applied to every keystroke, with one difference: a keystroke almost never rolls back. Instead, it gets merged with whatever other people did in the meantime.
All changes saved. 0 keystrokes sent in 0 messages.
Every operation moves through three states
The client keeps its own operations in a pending queue, split into two parts. Following one operation from key press to confirmation shows why.
- Local (Google's posts call these pending changes): applied to the local copy, not yet sent. New keystrokes join this group, and the editor can combine them, so typing
hellobecomes one insert of five letters rather than five operations. - In flight: sent to the server, waiting for its answer. Google's 2010 posts on Docs say the client never has more than one change in flight at a time. Everything typed meanwhile waits in the local group and goes out as the next batch.
- Confirmed: the server has accepted it, given it a revision number, and saved it. The client drops it from the queue. It is now part of the shared history.
Rarely, an operation is rejected: the user lost edit access, or it broke a rule like a size limit. The client undoes it locally and tells the user.
When someone else's operation arrives
This is the meeting point from the last chapter. Ben's operation arrives from the server. It was made on top of the shared history, but Asha's copy also contains her pending operations, which Ben has never seen. Applying Ben's operation as it is would repeat the big cat bug.
So the client adjusts Ben's operation to fit on top of Asha's pending ones, then applies it. It also adjusts her pending operations to sit after Ben's, the order the server already chose. With OT this is a transform function on pairs of operations; with a CRDT, operations name characters by stable ids, so they slot in without adjusting. Either way, the user's own text doesn't move.
Keep the cursor and selection still
Local echo isn't only about the letters. If Ben inserts a sentence above Asha's cursor, every character after it shifts down. The editor must move Asha's cursor and selection by the same amount, or her next keystroke lands in the wrong place. Editors run the cursor through the same mapping as the text: a cursor at offset 120 becomes 160 after a 40-character insert before it.
Show sync status honestly
Because the screen runs ahead of the server, the user can't tell from the text alone whether their work is saved. Show a quiet status that follows the queue: "Saving..." while anything is pending, "All changes saved" when the queue is empty, and "Offline: changes will sync when you reconnect" when the socket is down. Never show a spinner per keystroke.
window.addEventListener("beforeunload", (event) => {
if (pendingQueue.length > 0) {
// Browsers show their own generic text; the message is ignored.
event.preventDefault();
}
});
Check your understanding · Exercise 3
Asha has typed 12 letters that are still pending when Ben's insert arrives from the server. What must her editor do? Select all that apply.
A central server puts every operation in one order
Most collaborative products, including Google Docs, Figma, and Notion, send every operation through a server they control. The server's main job is to be the referee: it decides the one official order in which operations happened. Everything else in the design rests on that order.
Revisions: one counter per document
The server keeps a counter for each document called the revision. Every operation it accepts gets the next number: revision 41, 42, 43. The revision is the document's clock. "The document at revision 42" means one exact text that every client can rebuild.
Each client remembers the last revision it has seen, and sends its operations with that number as baseRev: "I made this looking at revision 41". The server compares it with its own head revision. If they match, the operation fits as it is. If the server is already at 43, the operation missed two others and must be adjusted against them, in the same way the client adjusts incoming operations, before it is accepted.
One server, one orderStep 1 of 5: Two edits at revision 41
big cat at revision 41. Asha inserts black and Ben deletes big . Each sees their own change at once: local echo.Two edits at revision 41: Both start from big cat at revision 41. Asha inserts black and Ben deletes big . Each sees their own change at once: local echo.
A worked example, message by message
Back to big cat, now at revision 41, with Asha and Ben both connected. Here is what actually crosses the wire.
// Asha -> server
{ "type": "ops", "clientId": "asha-7f", "seq": 18, "baseRev": 41,
"ops": [{ "kind": "insert", "pos": 4, "text": "black " }] }
// Ben -> server
{ "type": "ops", "clientId": "ben-c2", "seq": 5, "baseRev": 41,
"ops": [{ "kind": "delete", "pos": 0, "length": 4 }] }
// server -> Asha (her op is now revision 42)
{ "type": "ack", "seq": 18, "rev": 42 }
// server -> Ben (Asha's op, as revision 42)
{ "type": "remote", "rev": 42, "clientId": "asha-7f",
"ops": [{ "kind": "insert", "pos": 4, "text": "black " }] }
// Ben's op had baseRev 41 but head is 42: the server shifts it past
// Asha's insert. Deleting at 0 is before her insert, so it stays the same.
// server -> Ben
{ "type": "ack", "seq": 5, "rev": 43 }
// server -> Asha
{ "type": "remote", "rev": 43, "clientId": "ben-c2",
"ops": [{ "kind": "delete", "pos": 0, "length": 4 }] }
Asha applies Ben's delete to big black cat and gets black cat.
Ben's client receives Asha's insert while his delete is still in flight. It adjusts her insert to fit his pending delete: she inserted at 4, but 4 characters before that are gone in his copy, so the insert moves to 0. He gets black cat too. Both copies converge, and both intentions survive.
baseRev lower than the revision just before it means the operation was adjusted; client:seq is how resends are spotted.What the server does with each operation
- Check it. Is this user allowed to edit this document? Is the operation well formed and within limits, such as a 1 MB paste cap?
- De-duplicate it. If
clientIdplusseqis already in the log, this is a resend after a reconnect. Reply with the original revision and do nothing else. - Adjust it. If
baseRevis behind the head, transform it against every operation sincebaseRev. - Save it. Append it to the operation log in durable storage with the next revision number.
- Confirm and broadcast. Send an
ackto the author and aremotemessage to everyone else in the document.
Step 4 must come before step 5. If the server confirms and then crashes before writing, the author believes the edit is saved and it is gone. Saving first costs a few milliseconds per batch, one reason clients batch keystrokes.
One place per document
Ordering only works if one process decides the order for a given document. If Asha's socket lands on server A and Ben's on server B, and both servers accept operations, there are two referees and two orders. So all clients of a document are routed to the same process: the load balancer or a routing table maps the document id to one server, and a busy server holds thousands of documents in memory, each with its own queue.
If that process crashes, another one takes the document over, loads the latest snapshot and log from storage, and clients reconnect to it, as chapter 8 shows.
Check your understanding · Exercise 4
Why must the server write an operation to storage before sending the ack? Select all that apply.
Presence is not document state
A collaborative app sends two very different kinds of data, and the most common design mistake is to treat them the same way. Look at any shared document and you can split what you see in two.
- Document state is the content: the text, the shapes, the cells, the comments. Every change matters, changes must be applied in order, and all of it is saved forever (or until someone deletes it).
- Presence is who is here right now and what they are doing: avatars in the top bar, a colored cursor with a name tag, someone's selection highlighted, "Ben is typing". It only matters for a moment. Yjs calls this awareness; other tools say ephemeral state.
Opposite rules for the two kinds
Presence changes far more often than the document. A moving mouse produces 60 or more events a second. Losing one cursor update hurts nobody, because the next one, 50 to 100 ms later, replaces it. Losing one text operation corrupts the document, so the two need opposite treatment.
| Question | Document state | Presence |
|---|---|---|
| Examples | Text, shapes, cells, comments | Cursors, selections, who is online, typing indicator |
| If a message is lost | Resend it until it is acked | Ignore it; the next update replaces it |
| Ordering | Strict, one order set by the server | Only the latest value per person matters |
| Saved to the database | Yes, with history | Never; kept in server memory only |
| Typical rate per person | A few batches per second while typing | 10 to 20 updates per second while moving |
| When the person leaves | Their edits stay | Their presence is removed within seconds |
How presence flows
Presence usually rides on the same WebSocket as operations, as its own message type, but it skips the whole ordering pipeline: no revision number, no log, no ack. The server keeps a small in-memory map per document, from connection id to the latest presence value, and relays each update to the other clients.
{ "type": "presence", "clientId": "ben-c2",
"user": { "name": "Ben", "color": "#3b82f6" },
"cursor": { "anchor": 212, "head": 230 },
"typing": true, "ts": 1792405986634 }
- Send the whole value, not a change. Because messages can be dropped, each presence message replaces the last instead of patching it.
- Throttle on the sender. Send at most one update every 50 to 100 ms, and only if something changed. Other clients animate the cursor between updates so it still looks smooth.
- Expire on silence. Clients send a heartbeat every few seconds. If the server hears nothing for about 30 seconds, or the socket closes, it removes that person and tells the others.
- Anchor cursors to the content. A cursor at offset 212 is wrong once someone inserts text above it. Clients map remote cursors through incoming operations, or send positions as stable ids that a CRDT provides.
Why mixing them hurts
Imagine putting cursor moves into the operation log. Ten people with a cursor update every 100 ms add 100 entries a second to a log that otherwise grows by a few. History fills with noise, snapshots come more often, opening the document replays millions of old cursor positions, and every cursor move waits behind a database write. The opposite mistake is just as bad: sending text through the lossy presence path means dropped letters.
Presence also has privacy rules: a cursor reveals what someone is reading. Products often let viewers be anonymous and never store presence, so there is no record of who read which paragraph when.
Check your understanding · Exercise 5
Which of these belong in presence rather than in document state? Select all that apply.
Snapshots and history
If the document is the result of its log, opening it means replaying the log. For 300 operations that takes a millisecond. A design doc edited by a team for two years can have well over a million, which could mean tens of megabytes to download and seconds of replay on every open.
What a snapshot is
A snapshot is the full document saved at one revision: "here is the text as of revision 1,200,000". The server takes one every so often, for example every 1,000 operations or every few minutes of activity, usually in the background so writes don't wait for it.
Opening a document then becomes two reads: the newest snapshot, and only the operations after it. The client builds its copy from the snapshot, replays the short tail, and joins the live stream at the head revision. However old the document gets, the tail stays short, so opening time depends on the document's size, not on its age.
Opening a document, step by step
- The page loads and opens a WebSocket for document 42, or fetches over HTTP first so the content can render before the socket is ready.
- The server sends the newest snapshot (revision 1,200,000) and the 340 operations after it, up to the head at 1,200,340.
- The client builds the document and renders it. The user can read and type now.
- The client subscribes to new operations from revision 1,200,340 onwards, and starts sending presence.
Version history comes almost for free
Because every operation is saved with its author, time, and revision, the log already is the history. To show the document as it was last Tuesday at 15:00, find the newest snapshot before that time and replay operations up to it. Try it in the lab below: the further a revision is from a snapshot, the more operations the server replays.
- Group for people. Nobody wants to browse 40,000 keystrokes. History screens group operations by author and by gaps in time, such as "Asha, 14:02 to 14:31".
- Named versions like "Sent to legal" are just a label pinned to a revision, often with a snapshot so they open fast.
- Restoring is a new edit. Going back to Tuesday doesn't delete the log after it. The server computes the difference and appends it as new operations, so the restore itself shows up in history and can be undone.
Undo is personal
In a shared document, Ctrl+Z must undo your own last change, not Ben's, even if his came later. So undo can't be "step the log back one".
Instead, the client keeps a stack of its own operations. To undo one, it builds the inverse (delete what you inserted, put back what you deleted), adjusts it for everything that happened since, and sends it as a normal new operation. Redo works the same way in reverse.
Keeping the log from growing forever
A log of single keystrokes for a busy document grows by megabytes a week. Products compact old history: keep every operation for recent days, then only periodic snapshots, such as hourly and later daily, and drop the keystrokes between them.
Check your understanding · Exercise 6
A document has a snapshot every 1,000 operations and is at revision 52,430. What does a client download to open it? Pick one.
Choosing how to merge
Every collaborative product has local echo, an ordered path for content, a lossy path for presence, and snapshots. Where products differ is how they merge concurrent changes to the same thing. That choice depends mostly on what the document is made of: a long string of characters, or a tree of objects with properties.
Text versus objects
Text is the hard case. Characters have only positions, every insert shifts every later position, and people often type in the same sentence.
Objects are easier. A shape has a stable id like shape:17 and properties like x and fill. Changes to different shapes or different properties never conflict.
For the same property at the same moment, a simple rule works: the server's order decides and the later value wins. Figma described building multiplayer this way rather than with OT.
Decision How should concurrent changes to the same document be merged?
What we pick: For a rich text editor with a server you control, use server ordering with OT, or a CRDT library such as Yjs relayed through your server: both are proven, and the CRDT path is far less code. For a canvas or whiteboard, use per-property last-writer-wins on objects, and a text CRDT only inside text boxes. Choose a CRDT when offline editing for hours or peer-to-peer sync is a real requirement. Use locks only for coarse objects where waiting is acceptable.
Is a central server required?
With OT, yes in practice: the server's single order keeps the transform logic simple enough to be correct. With CRDTs, no in theory: copies can exchange updates directly, over WebRTC (the browser's peer-to-peer connection API) for example, and still converge.
In practice nearly every CRDT product still runs a server to store documents, check permissions, and catch up late joiners. The question is only how much the server decides. The next lessons, Operational transformation and CRDTs, show how each one merges, and Conflict resolution strategies covers object-level rules.
Check your understanding · Exercise 7
You're designing a whiteboard with sticky notes. People drag notes and type short text on them. Which designs make sense? Select all that apply.
What goes wrong in production
A demo with two tabs on one laptop works on the first try. Real users have flaky Wi-Fi, sleeping laptops, tabs open for a week, and documents with 200 viewers.
Dropped connections and offline edits
Connections drop all the time: a tunnel, a Wi-Fi handoff, a laptop lid. While the socket is down, local echo carries on, and new operations pile up in the local part of the queue. The status shows "Offline" so nobody is surprised.
On reconnect, the client says which revision it has. The server sends every operation since then. If the server saved the in-flight batch just before the drop, that list includes it under its new revision. The client spots its own clientId and seq and treats that entry as the missing ack, not as someone else's edit.
The client merges the other operations with its pending queue, exactly as in chapter 3, and sends what is still pending with the original clientId and seq. If a resent operation was already saved, the server recognizes the pair and acks it again without applying it twice. A two-second outage costs the user nothing.
clientId plus seq lets the server tell a resend from a new edit, so an operation saved just before the drop is never applied twice.Long outages are harder. Two people who each rewrite the same paragraph for an hour will converge, but into text that may not read well, because both rewrites survive side by side. Products choose differently: CRDT-based apps such as those built on Yjs merge anything, while some editors make the document read-only after a few minutes offline, or show a "your offline changes" review step.
The failures teams meet, and their fixes
| Failure | How you notice | Fix |
|---|---|---|
| Duplicate operation after a resend | Doubled text, like hellohello | Server de-duplicates on clientId plus seq; resends keep their ids |
| Copies silently diverge from a bug | Users report "I see different text" | Send a checksum of the document every few hundred revisions; on mismatch, reload from a snapshot and report the bug |
| Tab open for days, its revision long compacted | Server can't send the missing operations | Server replies "too old"; client loads a fresh snapshot and re-applies its pending operations on top |
| Old client code after a deploy | New operation types the old editor can't apply | A protocol version in the handshake; the server asks outdated clients to reload |
| Edit access removed mid-session | Writes keep arriving from a viewer | Server checks access on every batch, rejects, and the client switches to read-only |
| One huge paste, like 5 MB of text | Socket stalls, everyone lags | Size limit per operation; very large inserts upload over HTTP and enter the log as one reference |
| A document with 500 people open | Broadcast cost grows with every viewer, cursors flood the screen | Batch outgoing broadcasts every 50 ms, cap rendered cursors, and stop sending presence for viewers who aren't active |
| Server process for a document crashes | All its sockets close at once | Another process loads snapshot plus log; clients reconnect with jittered backoff so they don't all arrive in the same second |
The reconnect stampede
The last row deserves a closer look. A server holding 5,000 open documents restarts during a deploy. 40,000 clients see their sockets close in the same second and all reconnect at once, each asking for a snapshot. The new server falls over under that load, and the cycle repeats.
The fix is on the client: wait a random delay before reconnecting, such as 0 to 2 seconds the first time, then double the window on each failure up to about 30 seconds. Spread across 40,000 clients, a 2-second window turns one spike into a gentle slope. Deploys should also drain documents a few at a time rather than all at once.
Check your understanding · Exercise 8
Asha's laptop sleeps for 20 minutes with 3 unsent operations. The server compacted the log, so it no longer has the operations after her revision. What should happen? Select all that apply.
In real products, and the performance, security, and accessibility angles
How well-known products do it
- Google Docs uses operational transformation with a central server. Google's 2010 posts on the rewrite describe this lesson's model: a revision log of operations, pending and sent changes on each client, and never more than one unacknowledged change sent at a time.
- Figma described its multiplayer system in a 2019 engineering post. The server is the authority for each file, the file is a tree of objects with properties, and conflicts on the same property are settled by last-writer-wins. While a local change is unacknowledged, the client ignores incoming server values for that property, so the user's own change doesn't flicker back.
- ProseMirror ships a
prosemirror-collabmodule with a central authority and version numbers. Its server rejects changes based on an old version; the client rebases them on the newer ones and tries again. - Yjs is a widely used CRDT library with bindings for ProseMirror, Tiptap, CodeMirror, Monaco, and others. Its WebSocket provider relays binary updates through a server, and its separate awareness protocol carries presence, which is exactly the split from chapter 5.
Four different merge choices (transform, reject and retry, later value wins, data structure merges) and the same outer architecture every time.
Performance
- Batch outgoing operations. Collect keystrokes for one in-flight round trip, or for 50 to 100 ms, and send them as one message. Fewer messages, fewer database writes, and smaller history.
- Keep messages small. JSON is fine at first. Libraries like Yjs encode updates in a compact binary format, which matters for documents with many editors.
- Render only what changed. An incoming operation should update one paragraph, not re-render a 200-page document. For very long documents, render only the visible part.
Security
- Authenticate the socket. Check the session when the WebSocket opens, and check the
Originheader too: browsers can send cookies with cross-origin WebSocket handshakes, so without the check another site could open a socket as your user. - Authorize every batch, not just the connection. Access can be removed while a socket stays open for hours.
- Never trust the client's operations. Validate shape and size, enforce limits, and sanitize rich text the same way you would for a normal form, since an operation can carry any markup.
- Stamp identity on the server. The name and avatar in a presence message, and the author on each operation, come from the session, not from what the client claims.
- Rate limit. A buggy or hostile client can send thousands of operations per second. Cap per connection and disconnect offenders.
Accessibility
Remote cursors are purely visual, and a document that changes on its own can disorient screen reader users. A few rules help.
- Never move the user's focus or caret because of a remote change. Map the caret through the change instead, as in chapter 3.
- Announce people, not keystrokes. A polite live region (
aria-live="polite") can say "Ben joined" or "Ben left". Announcing every remote edit would drown out the user's own reading. - Don't rely on color alone. Pair each cursor color with a name label, and pick colors with enough contrast against the selection highlight.
- Respect reduced motion. Smoothly animated remote cursors are nice, but with
prefers-reduced-motion: reduce, jump them instead.
Check your understanding · Exercise 9
Which checks belong on the collaboration server? Select all that apply.
Using it in a system design interview
Real-time collaboration comes up in two ways. Sometimes it is the whole question: "Design Google Docs", "Design a shared whiteboard", "Design Figma's multiplayer". More often it is one requirement inside a bigger design: live comments on a task board, a shared shopping list, a pair-programming view.
The Google Docs guide in this section's practice problems walks through the full product version. This lesson gives you the parts to build it from.
In both cases, the interviewer is checking whether you know why it is hard. Candidates who jump to "I'd use WebSockets" have answered the transport question and skipped the real one. Lead with the problem: copies on every device, edits that cross in flight, and the need for one order.
What to draw
- Two browsers, each with three boxes: editor view, local document, pending queue.
- One collaboration server for the document, with an ordering step, an operation log, and a snapshot store.
- Two arrows between each browser and the server, labelled "operations (ordered, saved)" and "presence (lossy, memory)".
- An HTTP path for the first load: snapshot plus tail, before joining the live stream.
Then walk one keystroke through it: applied locally, queued, sent with baseRev, ordered and saved by the server, acked, broadcast, merged on the other client. If you can tell that story with real revision numbers, most follow-up questions answer themselves.
How deep to go at each level
| Level | What a strong answer covers |
|---|---|
| Junior | Why sending the whole document loses work. Operations over a WebSocket. Show my own typing at once. A sync status indicator. |
| Mid-level | Pending queue and acks. Revision numbers and a server that orders operations. Presence on a separate, throttled channel. Snapshots for fast loads. Reconnect with resend and de-duplication. |
| Senior | OT versus CRDT versus per-property last-writer-wins, chosen from the data model. One process per document and routing by document id. Persist before ack. Reconnect storms, version skew, divergence checks, compaction, security on the socket, and accessibility of remote changes. |
Common mistakes
- Saving the whole document on every change, which makes the last save win and silently drops other people's work.
- Waiting for the server before showing a keystroke.
- Putting cursor moves into the saved operation log.
- Using timestamps from users' devices to decide order. Device clocks drift by seconds; the server's revision counter is the clock.
- Explaining OT's transform rules in detail for 15 minutes and running out of time for loading, presence, and offline.
Check your understanding · Exercise 10
An interviewer asks: "Why not order edits by the timestamp each client attaches?" Which points make a strong answer? Select all that apply.
Summary
- Every person edits their own copy. Edits cross in flight, and an edit that names a position can be broken by one its author never saw. The goals are convergence, intention preservation, and causality.
- Send small operations, not whole documents. The document is the result of replaying its operation log.
- Local echo: apply your own edits at once, keep them in a pending queue until acked, and fit incoming edits around them without moving the user's text or cursor.
- A central server gives one order: it numbers each operation with a revision, adjusts stale ones, saves before it acks, and broadcasts. All clients of a document must reach the same process.
- Presence is not document state: cursors and selections are lossy, throttled, in memory, and expire; content is ordered, acked, and saved.
- Snapshots plus a short tail keep opening fast. The log gives history, named versions, restore, and per-person undo; compact old keystrokes.
- Pick the merge strategy from the data model: OT or a text CRDT for shared text, per-property last-writer-wins for objects, locks only for coarse objects.
- Plan for reconnects with resend and de-duplication, reconnect storms with jittered backoff, version skew, divergence checks, and a socket that checks
Originand permissions.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
How do you make typing feel instant in a collaborative editor when the server is 200 ms away?
Apply every keystroke to the local copy at once and render, then send it in the background. Sent but unconfirmed operations wait in a pending queue; keystrokes typed while a batch is in flight are combined into the next batch.
When other people's operations arrive, adjust them to fit on top of the pending ones (with OT), or let a CRDT place them by stable ids, and map the user's cursor through them. The user's own text never jumps. Show a quiet "Saving..." then "All changes saved" status instead of a spinner.
Question 2
Walk me through what happens when two people edit the same sentence at the same moment.
Both clients apply their own edit locally and send it with the revision they were based on, say 41. The server receives one first, saves it as revision 42, acks its author, and broadcasts it. The second edit still says base 41, so the server adjusts it against revision 42, saves it as 43, acks, and broadcasts.
Each client, on receiving the other's edit, adjusts it against its own pending edit before applying. Everyone applies the same edits in the same order, so all copies converge, and both intentions survive. The adjusting is OT; with a CRDT, stable character ids make it unnecessary.
Question 3
Why keep presence separate from document operations?
They need opposite treatment. Operations must be ordered, acked, resent on loss, and saved forever. Presence, like cursors and selections, changes 10 to 20 times a second, only the latest value matters, and losing one update is harmless.
So presence goes on a lossy, throttled path held in server memory, with whole values rather than changes and a heartbeat timeout. Putting it in the log would flood history and slow every load; putting text on the lossy path would drop letters.
Question 4
A document has been edited for three years. How do you keep it fast to open, and how do you show version history?
Save a snapshot of the full document every 1,000 operations or so. Opening loads the newest snapshot plus the short tail of operations after it, so load time depends on size, not age. Server-render a read-only view from the snapshot so text shows before the editor code loads.
History is the operation log read at an earlier point: load the snapshot before that time and replay up to it. Group operations by author and time for the history screen, make named versions labelled revisions, restore by appending new operations, and compact old keystrokes into periodic snapshots.
Question 5
When would you pick a CRDT over a central server with OT?
When editing offline for long periods is a core requirement, when peers should sync directly, or when you want a ready-made library like Yjs instead of writing transform functions. A CRDT merges updates in any order without a server deciding.
With a server you control and mostly online users, both work; OT keeps documents and history leaner, CRDT libraries save engineering time. Either way you still run a server to store documents, check permissions, and relay updates. For object-based tools like whiteboards, per-property last-writer-wins is often simpler than both.
Practice it
These design problems lean on real-time collaboration.
References
- What's different about the new Google Docs: Making collaboration fast Google's own walk-through of pending and sent changes, revisions, and transforming against the server.
- What's different about the new Google Docs: Conflict resolution The revision log and why operations, not whole documents, are stored.
- How Figma's multiplayer technology works A server-authoritative design with last-writer-wins per property, and how they avoid flicker.
- ProseMirror guide: Collaborative editing A central authority with version numbers, where clients rebase rejected steps.
- Yjs docs: Awareness and presence How a CRDT library keeps presence apart from document updates.
- Local-first software (Ink & Switch) The case for local copies as the source of truth, and where CRDTs fit.
- Google Docs design guide on Complete Frontend The full product design that builds on this lesson.
Read next
- Operational transformation (OT)A way to merge edits made at the same time: each incoming edit is rewritten, or transformed, to account for the edits it missed, so every copy ends with the same text. Google Docs is built on it.
- CRDTsConflict-free replicated data types: data structures that every device can edit on its own, offline or online, and that always merge to the same result without a central server deciding the order.
- Presence and live cursorsShowing who else is in a shared document and where they are pointing, in near real time: a stream of small, short-lived updates that must stay cheap, smooth, and honest about who has left.
- Conflict resolution strategiesWhat to do when two people change the same data: from last write wins per field, to version checks with
If-Match, to three-way merges and merge screens, chosen separately for each kind of data. - WebSocketsOne long-lived connection, opened by an HTTP handshake, on which browser and server can each send a message at any moment. The usual pick for chat, multiplayer, and collaboration, and the one where you own the protocol, reconnects, and scaling.
- Optimistic updatesShowing the result of a user's action at once, before the server confirms it, and quietly undoing it if the server says no: the trick that makes likes, checkboxes, and chat messages feel instant on any network.