System Design Primer · llms.txt
Beyond the primer: design questions
Written for this site, not part of The System Design Primer. The sources it draws on are listed under Further reading.
A collaborative editor lets several people change the same document at the same time and see each other's changes within a fraction of a second. There is no save button and no "someone else is editing this file" lock. Each person's keystrokes appear on their own screen at once, other people's edits stream in, and colored cursors show who is working where.
The hard part is concurrency. When two people edit the same sentence at nearly the same moment, each one's edit was made against a version of the text that didn't include the other's. The system has to merge both edits so that every screen ends up showing the same text, without locking the document and without throwing either edit away.
The primer lists "Design Google docs" in its appendix, with links to old Google Code projects that no longer exist. This chapter covers how these editors work: the two main merge techniques, Operational Transformation (OT) and Conflict-free Replicated Data Types (CRDTs); the WebSocket pipeline that carries edits; live cursors and presence; and how the document is stored. It builds on the primer's database and communication chapters and on the extra chapters on real-time updates, consensus and reliability patterns.
To see why this is hard, treat the text as an array of characters and have each client broadcast its edits as raw index operations.
Alice and Bob both start with "CAT". At about the same moment:
H at index 1, making "CHAT". Her operation is insert('H', 1).T), making "CA". His operation is delete(2).Each client applies its own edit at once, then receives the other's and applies it as is.
"CHT". Her insert shifted A into index 2, so Bob's delete(2) removed the A instead of the T."CHA", which is what both of them meant.The two copies have diverged, and nothing will bring them back together. An index only means something relative to the exact text it was computed against, and the two clients applied the operations in different orders. OT and CRDTs are two ways to fix this.
Operational Transformation was introduced by Clarence Ellis and Simon Gibbs in 1989, for the GROVE group editor. The client-server form most products use comes from the Jupiter system at Xerox PARC (1995). Google Wave was built on it, and Google Docs uses OT with a central server.
In OT, edits stay as index operations such as insert(char, pos) and delete(pos). A client applies its own edit locally at once, so typing never waits for the network. It then sends the operation to the server, tagged with the revision number of the document it was made against.
If the server has accepted other operations since that revision, it transforms the incoming operation against each of them before applying it. A transformation function takes two concurrent operations and returns a version of each that can be applied after the other:
transform(a, b) → (a′, b′) such that apply(apply(doc, a), b′) = apply(apply(doc, b), a′)
For text, the rules mostly shift positions. An insert before your position moves you right by one; a delete before your position moves you left by one. When two inserts land at the same position, a fixed tie-break (for example, the lower client ID goes first) decides which comes first, so every replica makes the same choice.
insert('H', 1) first, against revision 0. Nothing has happened since, so it applies it as revision 1. The server's text is "CHAT".delete(2), also against revision 0. Revision 1 has happened since.delete(2) becomes delete(3).delete(3) removes the T. The server's text is "CHA", as revision 2.delete(3) to Alice, whose text is "CHAT", and she ends with "CHA". It sends insert('H', 1) to Bob. Bob's client transforms it against his own delete(2): the insert is at index 1, before the deleted position, so it stays insert('H', 1), and Bob also ends with "CHA".Both clients converge on "CHA".
On the client side, the usual protocol (from Jupiter, and used by Google Wave) keeps at most one operation in flight. While waiting for the server's ack, the client buffers further keystrokes into a pending operation. Each operation the server sends is transformed against the in-flight and pending operations before it is applied locally. This keeps the client's bookkeeping small: it only ever has to reconcile against one server history.
A CRDT avoids transformation by never using positions that shift. Instead of "insert at index 1", an insert says "insert this character, which has this permanent ID, right after the character with that ID". IDs never change, so an operation means the same thing no matter what else has happened to the document. Libraries such as Yjs and Automerge implement CRDTs for text and JSON-like documents.
There are two main ways to give characters stable identities:
(client ID, counter) and records the ID of the character it was inserted after. Concurrent inserts after the same character are ordered by a deterministic rule on their IDs.Start with C = 1, A = 2, T = 3.
H between IDs 1 and 2, giving it ID 1.5 (tagged with her client ID). She sends "insert H at 1.5".T. He sends "delete ID 3".T: "CHA".H at 1.5" and places it between 1 and 2: "CHA".Neither operation had to be rewritten, and the order they arrived in didn't matter.
(1.5, alice) and (1.5, bob). Every replica sorts them the same way.Converging is not the same as preserving intent. If Alice types "Hello" and Bob types "World" at the same spot, some CRDT algorithms can interleave the letters into something like "HWeolrllod". Newer algorithms are designed to keep each run of typing together, and Martin Kleppmann's work on interleaving anomalies covers which ones do.
Google Docs uses OT with a central server. Figma's multiplayer system is a useful middle case: its engineers describe it as inspired by CRDTs but not a true CRDT. Figma's server is the central authority, which lets it drop some of the overhead a fully decentralized CRDT needs. Many CRDT-based products also run a server per document, for authentication, persistence and fan-out. The CRDT then isn't avoiding a server; it's making offline editing and merging much simpler.
These numbers are assumptions for a large product, not figures from any real one:
Network and connections are not the bottleneck. Each document's edits have to be put in order by one owner, so the design is shaped by routing every document to one coordinator and keeping the operation log durable.
(doc_id, revision). The worker acknowledges an edit to its sender only after the append is durable. Snapshots alone would lose the last 30 seconds of work on a crash; the log is what makes an acknowledged edit safe.Revision history falls out of the log and the snapshots. Version N is the nearest snapshot at or before N plus the operations up to N. Products don't show every keystroke as a version: they group operations into named or time-based checkpoints, such as "Alice, 10:42–10:51". Restoring an old version is done as a new edit that changes the current text to match the old one. It is not a rollback of the log, because other people may be editing at that moment.
What happens between Alice typing a letter and Bob seeing it, with OT:
k. Her editor applies it at once. Nothing waits for the network.{op: insert('k', 41), rev: 4}. If one is in flight, the keystroke joins the pending buffer, which goes out as one operation when the ack arrives. Fast typing is batched this way without a fixed timer.Within one region, steps 2 to 6 take roughly one network round trip plus a durable append: tens of milliseconds.
With a CRDT the flow is the same apart from step 3. The worker doesn't need to transform anything; it integrates the operation into its copy and assigns a sequence number for the log.
Clients notice the dropped connection or a timeout. The document is assigned to a new worker, which loads the latest snapshot and replays the log to the last durable revision. Clients reconnect and resend every operation that was never acknowledged, each tagged with a client ID and client sequence number so that the new worker can drop any it already has. Because the old worker only acknowledged durable operations, nothing acknowledged is lost.
Colored cursors and selections make collaboration feel live, but they are a different kind of data from the text:
(x, y) pixels, because each user's window width, font and zoom lay the text out differently. With OT it's an index transformed like any other position. With a CRDT it's a character ID, so it stays attached to the same character as text is inserted before it.Suppose someone edits on a plane and reconnects after making 200 edits while others made 1,000.
Either way the documents converge, but neither technique knows what the user meant. If two people rewrote the same paragraph separately for an hour, the merged result contains both sets of changes woven together and may read badly. Products handle this in the interface: after a long offline session they can highlight what changed, or offer the offline edits as suggestions for someone to accept.
An index only means something relative to the exact text it was computed against. If Alice inserts at index 1 while Bob deletes at index 2, Alice's insert shifts every later character right by one. When Bob's delete(2) reaches Alice, index 2 now holds a different character, so she deletes the wrong one. Bob applies Alice's insert to text without the T, which happens to be fine. The two copies end up different, and nothing later brings them back together.
OT keeps edits as index operations and rewrites (transforms) each incoming operation against the concurrent ones it missed. Getting the transformation functions right is far easier when one server puts every operation in a single order, so OT systems route all edits through a server.
A CRDT gives every character a permanent ID, so operations refer to IDs that never shift. Concurrent operations commute and can be applied in any order, with no transformation, and replicas can merge peer to peer. The price is per-character metadata and tombstones.
Products using either one usually still run a server per document, for access control, durable storage, revision history and fanning edits out to everyone. With a CRDT the server is a convenience. With OT it's what makes the ordering correct.
Only after the edit has been appended durably to the operation log. The acknowledgement tells the client it can forget the edit. If the worker acknowledged first and crashed before the append, or relied only on snapshots taken every 30 seconds, the edit would be gone and the client would never resend it. With the append first, a crash before the ack just means the client resends the edit to the new worker, and its client sequence number lets the worker drop a duplicate.
They have different requirements. Edits are the document: they must be ordered, never lost, and stored. Cursor positions are ephemeral: a lost update is corrected by the next one, and nobody needs a cursor's history. Sending cursor movement through the operation log would add a large number of durable writes for data nobody reads again. So cursors go through an in-memory channel (Redis pub/sub or the collab worker), are throttled on the client, and are anchored to positions in the text, not to screen coordinates.
It gives a consistent result: once both sets of edits are merged, both users see the same text. It doesn't give a meaningful one. Both rewrites are kept and woven together by position, which may read as nonsense. Convergence is about replicas agreeing, not about understanding intent. That gap is handled in the product, for example by highlighting large merged changes or turning offline edits into suggestions someone reviews.
Previous: Design a payment system and gateway · Next: Design a web search engine