Courses / System Design Interview / Chapter 34
Module 3 · Design Real Products · Chapter 34 of 39
Design Collaborative Document Editing
Builds on Databases: Replication, Partitioning and Choosing a Store, Consensus, Leader Election and Coordination, Designing a Key-Value Store, Blob Stores, Publish-Subscribe and Design a Group Chat App. Next up: Design a Notification System.
Part 1 · Requirements and Scale
The brief
The night before the college fest, the organising team's plan lives in a document file that hops around a WhatsApp group: plan_final.docx, plan_final_v2.docx, plan_final_ravi.docx. At 10 pm two copies disagree. One tells volunteers to meet at 5, the other at 6, and Meera's whole stage schedule exists only in a copy nobody else opened. A shared document ends this. All twelve volunteers type into one plan at the same time, see each other's cursors, and every screen ends up showing the same text. Making that hold when two people type in the same spot at the same moment is what this chapter designs.
The product: a collaborative document editor. Several people edit one document at once and see each other's changes and cursors live. Plan for about 10 million daily users. The fest team's plan, document d_91, edited by Asha on a laptop and Ravi on a phone, is the example we follow throughout.
This is one of the harder chapters. On a first pass through the course, read the brief, the high-level design and the 45-minute answer, then come back for the deep dives once the other products feel easy.
What we are building
The fest plan shows the whole product in miniature. Collaborative editing means many people changing one shared document at the same time, with every copy ending up identical. Sending keystrokes around is the easy part. The hard part is deciding what the document should say when two people change the same spot at once. Here is what the fest team, and 10 million people like them, need.
| Requirement | |
|---|---|
| Functional | Create, open and share a document with view or edit rights |
| Functional | Several people edit at once and see each other's changes live |
| Functional | See who else is in the document and where their cursors are |
| Functional | Browse version history and restore an older version |
| Functional | Keep editing offline and merge on reconnect |
| Non-functional | Your own typing appears instantly; other people's edits within about 200 ms |
| Non-functional | All copies converge to the same text, and an acknowledged edit is never lost |
| Non-functional | Most documents have 1 to 5 editors; support up to about 100 at once |
| Non-functional | Most of a document's editors are in one country; a few may join from another continent |
Two rows shape everything that follows: every copy must end up identical, and an acknowledged edit, one the server has confirmed as saved, must never be lost. The first rules out simply keeping the last save; the second decides when the server may say "saved".
Rich formatting, images and tables inside documents, comments, and search across documents. This chapter designs plain text edited by many people at once; the Going further part shows what rich text changes.
How big it gets
Before choosing machines, find out how much editing there really is, and how it spreads across documents. Assume 10 million daily users. At the evening peak about 10% of them have a document open: 1 million live connections, most of them reading or thinking rather than typing.
Try it first: about 20% of those 1 million people are typing at any moment, and each sends about 2 edit batches a second (keystrokes are grouped every 100 ms, and people pause). A busy document has about 2 typists. How many edits a second reach the whole system, and how many reach one document?
- 200,000 typists × 2 batches ≈ 400,000 edits a second across the system.
- 200,000 typists ÷ 2 per document ≈ 100,000 documents being edited, so each one sees about 4 edits a second.
- Huge in total, tiny per document.
The total is large, but each document is small. Four edits a second is nothing for one process, so every busy document can have a single owner that puts its edits in order. Scale comes from spreading 100,000 busy documents across many servers, not from splitting one document.
| Quantity | Working | Result |
|---|---|---|
| Collaboration servers | 1 M connections ÷ about 20,000 each (an assumption) | ≈ 50 servers, plus spares |
| Edit log writes | 400,000 /s × about 100 B | ≈ 40 MB/s at peak |
| A typical document | 10 pages ≈ 5,000 words | ≈ 30 KB of text |
| Snapshot writes | 400,000 edits/s ÷ one snapshot per 500 edits | ≈ 800 per second, about 24 MB/s |
Why 20,000 connections and not the 100,000 mostly idle sockets a chat server holds (the group chat chapter)? These connections are busy with keystrokes and cursor moves, and the server also holds documents in memory and transforms edits. As for latency, the 200 ms target splits into up to 100 ms waiting for a batch, 20 to 50 ms from Asha to the server, a few milliseconds for the log write, and 20 to 50 ms on to Ravi.
Part 2 · API, Data and the Big Picture
The API
Those numbers say each edit must be small and must go to the one server that orders its document. Opening a document is an ordinary HTTP call. Live editing uses a WebSocket, a long-lived two-way connection (the group chat chapter covers how servers hold many of them), because both sides push small messages at any moment.
GET /v1/docs/d_91 200 OK { "doc_id": "d_91", "version": 1840, "text": "Meet at 5 ..." } WS /v1/docs/d_91/live client → server { "type": "op", "base": 1840, "seq": 17, "op": { "insert": " pm", "at": 9 } } server → client { "type": "ack", "seq": 17, "version": 1841 } server → client { "type": "op", "version": 1842, "author": "u_7", "op": { "delete": 4, "at": 120 } } server → client { "type": "presence", "user": "u_7", "cursor": 120 } GET /v1/docs/d_91/history?before=1800 POST /v1/docs/d_91/restore { "version": 1500 }
`base` is the version the edit was made on; `seq` lets the client match each ack to its own edit.
- Edits are operations, not whole documents. "Insert ' pm' at position 9" is a few bytes; sending the 30 KB plan on every keystroke would multiply the traffic about 300 times.
- Every acknowledged edit gets the next version number, assigned by the server: 1841, 1842, and so on. The version is how both sides know what an edit was based on.
- Restore is a new edit, not a rewrite of history. It replaces the text with the old version's text as the next version, so every copy moves forward together.
The data model
Those version numbers need a durable home. Two kinds of data matter. An append-only edit log is the source of truth: every acknowledged operation, in order. Snapshots are copies of the whole text at some version, so nobody replays a year of edits to open the plan.
doc_id (pk)titlecreated_bylatest_versionlatest_snapshot_versioncreated_atdoc_id (pk)version (pk)author_idclient_idclient_seqopcreated_atdoc_id (pk)version (pk)blob_keycreated_atdoc_id (pk)user_id (pk)role: view or edit- doc_ops doc_id → documents
- snapshots doc_id → documents
- permissions doc_id → documents
Partition everything by doc_id (the databases chapter covers partitioning). All of one document's operations then live together, ordered by version, so opening one is a single blob read for the latest snapshot plus a single range read for the operations after it. The primary key (doc_id, version) does a second job. Every append is an insert-if-absent, which relational databases and many key-value stores support, so two writers can never both store version 1841. The high-level design uses that to stop a stale owner.
| Data | Where it lives | Why there |
|---|---|---|
| Edit log | a partitioned, replicated database with insert-if-absent, keyed by (doc_id, version) | ordered appends, range reads, durability, fencing |
| Snapshots | the blob store, one file per snapshot | cheap, written once, always read whole |
| Documents and permissions | a relational database | small rows; joins for sharing screens |
| Presence and cursors | the owner server's memory only | changes many times a second, useless a minute later |
Permissions are checked on connect and on change. The owner checks the user's role when the WebSocket opens and rejects edits from viewers. When someone's access is removed, the owner closes their connection at once instead of waiting for a reconnect.
The high-level design
With operations, versions and a log in place, the question is who puts each document's edits in order. The answer is one owner per document. The gateway routes every WebSocket for d_91 to the collaboration server that owns it. That server keeps the plan in memory, orders each incoming edit, writes it to the log, and only then acknowledges it and forwards it to the other editors.
Watch for the owner's order: log the edit, then ack it, then forward it
Read it as steps
The owner of the fest plan logs, acks and forwards every edit. When it crashes, a new owner rebuilds the plan from the log and fences the old one out.
The order is the lesson: log, then acknowledge, then forward. Ack before logging, and a crash leaves Asha believing a lost edit was saved. Forward before logging, and Ravi keeps an edit a new owner never heard of. Because the log holds every acknowledged edit, any server can rebuild the plan exactly.
- Finding the owner. The gateway hashes
doc_idonto a ring of live collaboration servers, the consistent hashing idea from the key-value store chapter, so adding a server moves only a small share of documents. - Loading and unloading. The first editor to open a document makes its owner read the latest snapshot and later operations into memory; a few idle minutes after the last editor leaves, the owner drops it.
- Exactly one owner. Each server holds a lease from a coordination service (the coordination chapter) and renews it every few seconds. A new owner's first append is a takeover record, an entry with no text change, in the next version slot. An old owner that was only paused then fails its next insert-if-absent and steps down: the version number plays the fencing token.
Part 3 · Deep Dives
Two people typing at once
This is the problem that makes the product hard: a chat app only appends messages, but here two people can change the same spot at once.
The owner puts edits in order, but order alone does not settle what the text should say. Edits name positions: "insert ' pm' at position 9" means after the ninth character. Think of directions that say "the ninth house on the street". If six new houses go up at the start of the street, the same house is now the fifteenth, and the old directions lead to the wrong door.
Say the plan's first line reads Meet at 5. At the same moment, Asha types Let's at the start and Ravi types pm at the end, position 9. Each app applies its own edit at once, called local echo, so typing never lags, then sends it to the other through the server.
| Asha's screen | Ravi's screen | |
|---|---|---|
| Start | Meet at 5 | Meet at 5 |
| After their own edit | Let's Meet at 5 | Meet at 5 pm |
| After the other's edit, as sent | Let's Mee pmt at 5 | Let's Meet at 5 pm |
| Result | garbled, and different | correct |
The copies have diverged: the two now see different text and neither knows it. Ravi's position 9 was right for the text he saw, but Asha's copy had six more characters in front. His edit must be transformed to 9 + 6 = 15 before Asha applies it.
Convergence: once everyone has received every edit, all copies are identical. A good system also tries to keep intent: Ravi wanted ' pm' right after the 5, so it should land right after the 5 on every screen. Who shifts which edit, and against what, is the next page's question.
Operational transformation
Operational transformation (OT) rewrites a late edit so that it means, on the current text, what it meant to its author. The owner decides the order: when an edit arrives with a base version older than the latest, the owner transforms it against every edit applied since that base, then logs and forwards the result.
Predict before you watch: the line reads Meet at 5 at version 1. At the same moment, Asha inserts ' pm' at position 9 and Ravi inserts ' sharp' at position 9. Asha's edit reaches the server first. What does the server store for Ravi's edit, and what does every screen end with?
- A position alone cannot say whose text goes first, so every copy needs the same tie-break: here, the edit the server ordered first goes on the left.
- The server stores Ravi's insert at 9 + 3 = 12, just after ' pm'.
- Every screen ends with
Meet at 5 pm sharp.
Watch for the tie-break: both insert at position 9 of "Meet at 5" at once
Read it as steps
Two inserts at the same spot: the server orders Asha's first and shifts Ravi's past it, and Ravi's app shifts his waiting edit the same way.
Both sides transform. The owner transforms a late edit against everything logged since its base. Each app transforms incoming edits against its own unacked ones, and shifts those too: Ravi's waiting ' sharp' moved from 9 to 12 on his own screen. To keep this simple, an app has at most one batch in flight and buffers new keystrokes until the ack arrives.
The work is small. At about 4 edits a second per document and 100 to 200 ms for a round trip to the owner, an edit usually overlaps zero or one other, so it needs one transform or none, each a few comparisons. The single owner keeps it this simple: OT with no central order is hard to get right, and some published peer-to-peer designs were later shown to fail in rare cases.
A transform by hand
The scene used one rule. Here is the full set for plain text, then one transform to do yourself. Each row says what happens to edit B when edit A, made on the same version, was applied first.
| Edit A, applied first | What happens to edit B |
|---|---|
| Insert of n characters before B's position | B moves right by n |
| Insert at the same position as B's insert | tie-break: the edit ordered first stays left, so B moves right by n |
| Insert inside the range B deletes | B is split around A's text, so A's text survives |
| Delete of m characters before B's position | B moves left by m |
| Delete of a range that contains B's insert position | B moves to where the deleted range started |
| Delete that overlaps B's delete | B deletes only the characters still there |
| Any edit after B's position | no change |
Check any rule with one test: applying A and then the transformed B must give the same text as applying B and then the transformed A. When both orders agree, every copy converges whichever edit it receives first.
Try it by hand: the line reads Meet at 5 at version 1. Asha deletes Meet (5 characters at position 0) while Ravi inserts ' pm' at position 9. The owner orders Asha's delete first. What does it store for Ravi's insert, and what does each screen show?
- The delete removes 5 characters wholly before position 9, so Ravi's insert moves left by 5: insert ' pm' at 4.
- The owner's copy is
at 5; ' pm' at 4 makesat 5 pm. - Ravi's screen shows
Meet at 5 pm. The delete lies wholly before his insert, so it applies unchanged and leavesat 5 pm. Both orders agree.
def transform_insert(b, a, a_first): # b: an insert. a: applied first, made on the same version. if a.kind == "insert": if a.at < b.at or (a.at == b.at and a_first): return Insert(b.at + len(a.text), b.text) return b if a.at + a.n <= b.at: # whole deleted range is before b return Insert(b.at - a.n, b.text) if a.at < b.at: # b sat inside the deleted range return Insert(a.at, b.text) return b # deleted range is after b
Four rows of the table as code. The owner and every app run the same function, which is why they agree.
CRDTs: ids instead of positions
OT repairs positions after the fact. A CRDT (conflict-free replicated data type) avoids positions altogether. Instead of "the ninth house on the street", every house gets a permanent number painted on its gate, so new houses never change anyone's address.
In a text CRDT every character gets a permanent unique id: here, the device that typed it plus that device's counter. An insert says "put ' pm' after character a9", not "at position 9". This is the plan's line as a CRDT holds it:
id a1 a2 a3 a4 a5 a6 a7 a8 a9
char M e e t _ a t _ 5a is Asha's laptop and 1 to 9 its counter; _ marks a space.
Now Asha types ' pm' and Ravi types ' sharp', both after a9, at the same moment. Each new character is anchored to the one before it, so each word is a chain hanging off a9. A device's counter always moves past the highest it has seen, so Asha's chain starts at id a10 and Ravi's at r10. Every copy orders two chains on one anchor by a fixed rule: higher counter nearer the anchor, and on a tie, the higher device name. r10 beats a10, so every copy reads Meet at 5 sharp pm, whichever edit arrives first, with no server deciding.
Deletes do not remove a character; they mark it deleted, a tombstone, because a slower edit may still be anchored to it. If Ravi deletes the 5 while Asha's ' pm' is in flight, a9 stays as a tombstone and ' pm' still lands after it. An edit anchored to a character a copy has not received yet waits until that character arrives.
Meet at 5 sharp pm may not be what either person wanted, but OT's tie-break cannot read minds either: both approaches promise identical copies and leave wording to people. Many CRDT apps still use a server to relay and store edits; they just do not need it to decide the order. What does all this bookkeeping cost?
What a CRDT costs
The permanent ids that free a CRDT from a central order are also its price. Count them for the fest plan, a 30 KB document. Three costs show up in practice.
- Metadata per character. Each character carries its own id and its anchor's id, roughly 8 to 16 bytes each: 16 to 32 bytes of bookkeeping for one byte of text. Stored naively, the 30 KB plan carries about 1 MB of ids, and several times that in memory.
- Tombstones that pile up. Every character ever typed stays. If the team typed 300 KB over a month of rewrites and kept 30 KB, the CRDT holds ten times the visible text.
- Clean-up needs everyone. A tombstone can be dropped only when every copy has seen the delete, since an older edit may still point at it. One phone that stays offline for a month blocks clean-up for that document.
Libraries cut the first cost sharply by storing a run of characters typed in one go as a single item with a range of ids, which brings the metadata for normal typing down to a small multiple of the text. They cannot remove the history: a heavily rewritten document stays large, and opening it means loading that state, not just the text.
| Operational transformation | CRDT | |
|---|---|---|
| Who orders edits | a central owner per document | nobody: concurrent edits merge in any order |
| Offline and peer to peer | possible, but a long absence means many transforms at one owner | a natural fit: copies merge whenever they meet |
| Extra data | small: the text plus a version number | an id per character, plus every tombstone |
| Main difficulty | getting the transform rules right | memory, load time and tombstone clean-up |
Choose OT with an owner when a server is always in the path; choose a CRDT when copies must merge without one. The fest plan is edited in browsers and apps that are almost always online, so this design keeps OT and its small per-edit cost.
Edit log, snapshots and history
Whichever method orders the edits, the ordered result must survive crashes and answer "what did the plan say yesterday?". The edit log is the document's diary: every acknowledged operation is appended with its version number and never changed. Replaying it from version 1 rebuilds the plan exactly, but after months that could mean millions of operations, so the owner also writes snapshots, bookmarks that hold the whole text at one version.
- When to snapshot: every 500 operations, or after a few minutes of quiet, whichever comes first. The owner writes the full text to the blob store and records the version in
snapshots. - Opening a document: read the latest snapshot, then the operations after it, rarely more than about 500.
- Version history: group operations into sessions (a 30-minute gap starts a new one) and show each session's authors and time. An old version is the nearest snapshot before it, replayed forward.
- Compaction: after, say, 30 days, keep hourly or daily snapshots and drop the operations between them. Old history becomes coarser but cheap.
open d_91 latest version 1840 latest snapshot 1500 (30 KB file from the blob store) ops to replay 1501 to 1840 (340 ops, about 34 KB) time to apply a few milliseconds in memory
Snapshots keep opening fast no matter how long a document has existed.
Undo is per person. Undo should take back your own last edit, not whatever anyone typed last. The app keeps a stack of the user's own operations; undo builds the inverse (a delete for an insert), transforms it past every edit made since, and sends it as a new edit. Restore works the same way, so history only moves forward.
Acknowledge an edit only after the log write has succeeded on more than one replica, in different zones. It costs a few milliseconds. A fast ack that is later lost means a volunteer's sentence silently disappears, the worst bug this product can have.
Presence, cursors and offline edits
Two kinds of data are left: the kind that is useless a minute later, and the kind that arrives hours late. Presence (who is here) and cursors (where they are) change many times a second, so they live only in the owner's memory and never touch the log. The owner forwards cursor moves a few times a second at most and drops a user whose connection misses heartbeats for about 30 seconds.
Cursors shift the same way edits do. If Ravi's cursor is at position 40 and Asha inserts 10 characters at position 5, Ravi's cursor must move to 50 on everyone's screen. The same transform rules apply.
Offline edits. When the connection drops, the app keeps working: it applies edits locally and queues them in the device's storage with the last version it saw. Say Ravi edits the plan on a two-hour train ride with no signal and returns with 500 queued operations while the plan moved on by 2,000. On reconnect the owner transforms his queue against those 2,000, and his app transforms the 2,000 against his queue: about a million rule checks, well under a second of CPU.
- The real cost is meaning. The merge converges, but where both sides rewrote the same paragraph the text may read oddly. Show the user what changed while they were away.
- Lost access. If the user's edit right was removed meanwhile, the owner rejects the queue. Offer to save their text as a new document rather than throwing it away.
- Repeats. A reconnecting app may resend an edit the owner already stored. Each edit carries a client id and sequence number, so the owner recognises repeats, skips them and acks them again.
Part 4 · Failures and Trade-offs
What can fail
Everything so far assumed machines stay up. Most failures here pause editing for a few seconds; the promise to keep through all of them is that no acknowledged edit is lost.
| Problem | Effect | Fix |
|---|---|---|
| Owner server crashes | editors pause for a few seconds | a new owner loads snapshot plus log and writes its takeover record; apps reconnect and resend unacked edits |
| A paused owner wakes up | two servers think they own a document | its next append hits a taken version slot, so it steps down and closes its connections |
| A document with thousands of viewers | one server holds thousands of sockets | serve viewers through a fan-out tier; keep editors on the owner |
| A slow or flaky client | its outgoing buffer grows on the server | disconnect it past a limit; it reloads from a snapshot on reconnect |
| Log store slows down | acks slow down for every editor | batch log writes per document; alert on write latency |
| A zone fails | documents owned there pause | the log is replicated across three zones, so owners in other zones take over in seconds |
| A region fails | every document homed there stops | promote a standby region fed by an asynchronous copy of the log; it may lack the last second or two, so an app that sees the version go backwards offers to re-add its missing text |
| A deploy restarts the fleet | 1 M sockets reconnect and every open document reloads | restart a few servers at a time; each stops taking edits, finishes its log writes and hands off its documents; apps reconnect with jitter over 30 to 60 s |
The region row is the one place the promise bends. Copying to another region before every ack would add a cross-region round trip, about 30 ms even from Mumbai to Delhi, and let a slow standby slow every editor, so most designs accept losing the last second or two in a rare whole-region loss. The viewer row matters on fest day, when the schedule is projected around campus and 5,000 phones follow it while 3 organisers edit. Editors need the owner to order their edits; viewers only need the ordered stream. The owner publishes each version to a topic (the pub-sub chapter), a fan-out tier pushes it to viewers, and the owner keeps 3 sockets, not 5,003.
Trade-offs and running it
Each choice above buys something and costs something. Here they are side by side, with the one that remote editors feel explained below.
| Decision | Choice | Cost |
|---|---|---|
| Conflict handling | OT with a per-document owner | needs the owner; long offline merges read oddly |
| When to acknowledge | after a log write in more than one zone | a few milliseconds more per edit |
| Snapshot frequency | every 500 operations | more blob writes, but fast opens |
| Presence | memory only, throttled | lost on a crash, which nobody minds |
| Where the owner lives | one region per document, near most of its editors | about 150 to 200 ms more per ack for editors on other continents |
Editors far away. A New York editor on a plan owned in Mumbai still sees their own letters at once, thanks to local echo; only the ack takes about 200 ms, and the app simply sends bigger batches meanwhile. Others' edits reach them within about 250 ms. Do not give each region its own owner: two owners mean two orders of the same edits, which needs consensus between regions or a CRDT. If the team moves for weeks, hand ownership to a nearer region at a version boundary.
The replicated edit log dominates the bill: about 400,000 writes a second at peak, each kept in three copies, takes far more machines than the 50 or so collaboration servers. Compacting old operations after 30 days keeps its storage in check.
Part 5 · Going further
Rich text: edits on a tree
Optional: the fest plan has headings, bold times and lists, and supporting them changes the transform function but not the architecture. Store the document as a tree: blocks such as paragraphs, headings, list items and tables, each holding runs of text with marks such as bold or a link.
| Operation | Example | The hard concurrent case |
|---|---|---|
| Insert or delete text | type ' pm' in block 3 at offset 9 | as for plain text, but a position is now a path: block, then offset |
| Split or join blocks | press Enter in the middle of a paragraph | text typed after the split point must move into the new block |
| Add or remove a mark | make characters 5 to 15 bold | a split at 10 at the same time: both halves must end up bold |
| Change a block's type | turn a paragraph into a heading | two people pick different heading levels: the edit ordered last wins |
| Insert, delete or move a block | drag a list item; delete a paragraph | someone types into the deleted paragraph: drop the text or keep the paragraph, and decide once |
The owner still orders every edit, so nothing in the design above changes; only the transform function grows. Five operation types make 25 ordered pairs, each with its own cases. Marks add a question plain text never had: is a letter typed right after a bold word bold too? Many editors say yes for bold and no for links.
CRDT versions use the same trick as before: a mark is anchored to the ids of its first and last characters, so a bold range keeps covering the right words while others type inside it.
Get plain text right, then treat rich text as the same idea on a tree: positions become paths and the rule table grows. Teams usually build on a well-tested editor library's operation model rather than writing these rules from scratch.
Part 6 · Interview kit
The 45-minute answer
A 45-minute round leaves about 40 minutes of design after introductions. Here is this product on that clock.
| Minutes | What you say and draw | Key number or decision |
|---|---|---|
| 0–5 | Requirements: live co-editing, cursors, history, offline. Non-functional: own typing instant, others' within about 200 ms, no acknowledged edit lost | 1 to 5 editors per document, up to about 100 |
| 5–8 | Estimates that decide something: 1 M live connections, about 400,000 edits a second | about 4 edits a second per document, so one owner each |
| 8–13 | API: open over HTTP, edit over a WebSocket, each edit an operation with a base version. Data: the (doc_id, version) log, snapshots in a blob store | operations, never whole documents |
| 13–22 | Draw the gateway, owner by consistent hashing, edit log, snapshot store, coordination service for leases | log, then ack, then forward |
| 22–37 | Deep dive: two inserts at one spot with a tie-break, a delete shifting an insert left, then CRDTs and their cost | 9 + 3 = 12; about 1 MB of ids for 30 KB of text, naively |
| 37–40 | Wrap-up: owner crash and takeover record, zone and region loss, monitoring, rich text next | a claimed version slot fences a stale owner |
Pick the deep dive from the question. Two people in one spot: OT and the tie-break. Editing on a train with no signal: offline merging, and CRDTs if offline is the main use. Last Tuesday's version, or what survives a crash: the edit log and snapshots.
- Say
- "Each document has one owner that orders its edits. At about 4 edits a second per document, one process is plenty; scale comes from spreading 100,000 busy documents."
- Say
- "I write each edit to a log replicated across zones before I ack it. That costs a few milliseconds, small next to the 100 ms batching window."
- Say
- "OT with an owner suits an always-online editor; a CRDT suits offline-first sync, at the cost of an id per character and tombstones."
- Avoid
- "Last write wins on the whole document." Two people typing at once would overwrite each other's work.
- Avoid
- "Lock the paragraph while someone types." Others are blocked, and a crashed editor's lock blocks everyone until it expires.
Interview drill
Interviewers often change one thing and watch how you adapt. Try each before opening the answer.
Usage grows 10×: 100 million daily users and 10 million live connections at peak. What changes?
- Per document, nothing: a busy document still sees about 4 edits a second, so one owner each still works.
- Collaboration servers grow to roughly 500 at 20,000 connections each.
- The log takes about 4 M writes a second: about ten times the partitions, with group commit (many appends per disk flush).
- Deploys and ring changes move more documents at once, so restart in smaller steps.
Product adds comments anchored to text that others keep changing. What changes in the data model?
- A
commentstable keyed by (doc_id, comment_id) holds each thread and an anchor: start, end and the version they refer to. - Anchors move like cursors: the owner transforms them with every edit, or a CRDT ties them to character ids.
- If the anchored text is deleted, keep the comment and mark it detached.
Finance asks you to halve the cost. Where do you look first?
- The edit log, since it dominates the bill: compact after 7 days instead of 30, accepting coarser history beyond a week.
- Move old snapshots to an archive tier, 20 to 50 times cheaper.
- Serve viewers from a cheaper fan-out tier and unload idle documents sooner.
- Common miss
- Splitting one document's edits across servers for scale. Four edits a second needs no splitting, and two servers ordering one document would need consensus.
- Common miss
- Writing cursor moves to the edit log. They change many times a second, are useless a minute later, and would multiply the log's writes.
Wrap-up
The whole chapter on one page, for revision.
Next, the notifications chapter, Design a Notification System, moves from connections you hold yourself to messages you hand to outside providers: push, SMS and email.
Part 7 · Check yourself
Quick check
Q1The fest team's first prototype saved the whole plan on every change, and the last save won. Sentences vanished when two people typed at once. What is wrong, and what would it cost at full scale?
Think it through, then check.
- Each save replaces the whole text, so a save landing moments later erases the other person's sentence.
- A 30 KB document per batch at 400,000 batches a second is about 12 GB/s of writes, against about 40 MB/s for 100-byte operations.
- Send operations with a base version; let the owner order and transform them.
Q2A partner college in London co-organises the fest, and half the editors of d_91, owned in Mumbai, are now there. What do they notice, and what would you change?
Think it through, then check.
- Their typing still appears at once; only acks arrive about 150 ms later.
- Edits cross between cities in a couple of hundred milliseconds, which still feels live; OT is unchanged.
- Keep one owner. If London becomes the main team, hand ownership over at a version boundary.
Q3Inspectors edit shared reports for days in villages with no signal, then sync. OT with an owner, or a CRDT, and what does the choice cost?
Think it through, then check.
- A CRDT: copies merge in any order whenever they meet, with no server deciding.
- The cost is ids per character and tombstones that cannot be cleaned up while any tablet is still offline.
- OT could work, but each return means a large merge through one owner.
Q4Server 1 freezes for 20 seconds in a garbage-collection pause; its lease lapses and server 2 takes over d_91 at version 1840. Server 1 wakes and appends version 1841. What stops two histories?
Think it through, then check.
- Server 2's first append was a takeover record at 1841, so server 1's insert-if-absent fails.
- The failure tells server 1 it lost ownership; it stops and closes its connections.
- Its editors reconnect to server 2 and resend unacked edits; nothing acknowledged is lost.
Q5A newsroom document is edited by 3 reporters and watched live by 3 lakh readers. Which building block carries updates to readers, and why not the owner?
Think it through, then check.
- Pub-sub (the pub-sub chapter): the owner publishes each version once, and fan-out servers push it to readers.
- 3 lakh sockets would swamp an owner sized for 20,000; at about 50,000 per fan-out server, 6 machines hold them.
- The owner keeps only the 3 editors, and a fan-out failure never stops editing.