Design a Collaborative Editor

Let many people edit one document at once: merging edits with OT or CRDTs, one server per document, an operation log with snapshots, and cursors.

Case study32 minLesson 6 of 8
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

Three people have the same document open. Ana fixes a typo while Ben adds a sentence above it and Chen watches both cursors move. Each types into their own copy with no delay, and a moment later every copy shows the same text. Nobody presses Save and no edit is lost.

Interviewers ask this because the boxes are simple and the core problem is not: two edits made at the same moment against the same text must merge into one result, on every screen, whatever order the network delivers them in. The steps follow A Framework for Any Design; try each "Your turn" box before you read on.

Clarify the problem

The first five minutes
Interviewer

Design a collaborative document editor.

You

Plain text or rich text? And do we need comments and version history?

Interviewer

Rich text, but reason about plain text. Version history, yes. Comments are out.

You

How many people edit one document at once, and how big is the whole thing?

Interviewer

Usually one to three, sometimes up to 100 in a meeting. About 20 million daily users; at peak 2 million have a document open.

You

Then the core requirement is convergence: everyone ends with the same text and no keystrokes vanish. I'll aim for zero delay when typing, other people's edits within about 200 ms in one region, and no acknowledged edit ever lost. I'll handle short offline periods.

  • Functional: open a document; type and see your change instantly; see other people's changes and cursors live; keep working offline and sync on reconnect; browse version history.
  • Non-functional: all copies converge; remote edits appear within about 200 ms in one region; an acknowledged edit is never lost; up to 100 editors per document; a document opens in under a second.
  • Out of scope: comments, search, sharing screens, rich-text layout.

Estimate the scale

Editors do not send a message per keystroke. The client batches the last 100 ms or so of typing into one operation, such as "insert the at position 4". Assume an open document sends one operation every 2 seconds on average, counting pauses.

Estimate: Operations per second at peak

How many operations per second reach the servers at peak?

You know
  • 2 million people have a document open at peak
  • Each sends one operation every 2 seconds on average

Show the working
  1. 2,000,000 ÷ 2 s = 1,000,000 operations a second

Estimate 1 million/s (anything from 300 thousand/s to 3 million/s is a good answer)

Each one must be ordered against the others on the same document, which is what the design is about.

  • Daily volume: with a document open about an hour a day, 3,600 ÷ 2 ≈ 2,000 operations per user, so 20 million users × 2,000 = 40 billion, an average of 40 × 109 ÷ 86,400 ≈ 460,000 a second. The peak of 1 million is about twice that.
  • Fan-out: with 2 editors per open document, there are 1 million open documents. Each operation is acknowledged to its author and sent to one other person: 2 million outgoing messages a second.
  • Connections: 2 million WebSockets. At an assumed 50,000 per server (load test it), 40 servers.
  • Snapshots: assume 1 billion documents of about 50 KB: 50 TB for the latest copy of each.
Estimate: Operation log per day

How much operation log is written per day?

You know
  • 40 billion operations a day
  • About 100 bytes per operation (position, text, author, revision, time)

Show the working
  1. 40 × 109 × 100 B = 4 × 1012 B = 4 TB a day
  2. Kept for 30 days: 120 TB, or 240 TB on a leader and its replica

Estimate 4 TB (anything from 1 TB to 15 TB is a good answer)

The log grows far faster than the documents, so it must be folded into snapshots.

Your turn: Define the API

Write the messages a client and server exchange while editing. What must each operation carry so the server knows what it was based on?

Compare with a model answer

An HTTP call loads the document at a revision; then a WebSocket. Each operation carries the revision it was typed against and a client sequence number. The server acks with the operation's final revision and sends everyone else's operations with theirs.

Define the API

HTTP and WebSocket messages
GET /docs/{id}
  200 { "rev": 1042, "content": "...", "title": "Q3 plan" }
  403 Forbidden    no access     404 Not Found
 
WSS /docs/{id}/live?since=1042
  client -> server
    { "type": "op", "base_rev": 1042, "client": "c7", "seq": 17,
      "ops": [ { "insert": "the ", "at": 4 } ] }
    { "type": "cursor", "rev": 1043, "at": 88 }
  server -> client
    { "type": "ack", "seq": 17, "rev": 1043 }
    { "type": "op", "rev": 1044, "author": "ben", "ops": [...] }
    { "type": "cursors", "users": [ { "user": "ben", "rev": 1044, "at": 12 } ] }
    { "type": "reload" }   since is older than the kept log; fetch a snapshot

The decision worth arguing: each client keeps at most one operation in flight. Until seq 17 is acknowledged, new typing is buffered and sent as one operation after the ack. The server then only adjusts an incoming operation against what was committed since its base_rev. The pair (client, seq) makes a resend harmless: the server remembers the last seq applied per client and drops duplicates.

Your turn: Model the data

What do you store so a document opens quickly, can be replayed for history, and can be caught up after a reconnect?

Compare with a model answer

An append-only log of operations keyed by document and revision; periodic snapshots of the full text so opening does not replay everything; and a small metadata record per document. Presence is not stored at all.

Model the data

Records
documents                         -- metadata, relational
  doc_id        BIGINT PRIMARY KEY
  owner_id      BIGINT
  title         VARCHAR(200)
  snapshot_rev  BIGINT            -- newest snapshot
  updated_at    TIMESTAMP
 
doc_ops                           -- about 100 B a row
  doc_id        BIGINT
  rev           BIGINT            -- 1, 2, 3 ... per document, no gaps
  author_id     BIGINT
  client_id     VARCHAR(16)
  client_seq    INT
  op            BYTES             -- the transformed operation
  created_at    TIMESTAMP
  PRIMARY KEY (doc_id, rev)
 
snapshots  -- blob store, key "docs/{doc_id}/{rev}", about 50 KB

Access patterns: append the next revision of document 42, read its operations after revision 1000, fetch its newest snapshot. All stay inside one document, so partition everything by doc_id.

For doc_ops, use a store with one leader per shard and a unique primary key, such as a sharded relational database. "Insert revision 1043" then fails cheaply if 1043 exists, which the deep dives use to stop two servers writing the same revision. At 1 million inserts a second and an assumed 20,000 per shard, that is 50 shards, each with a synchronous replica so an acknowledged operation survives losing the leader. A leaderless store fits the range reads too, but there "insert if absent" costs a consensus round on every operation. Snapshots are write-once blobs, so they go to blob storage.

Your turn: High-level design

Sketch the boxes. Where do the WebSockets end, which server decides the order of operations for document 42, and how do all its editors reach that server?

Compare with a model answer

Gateways hold the sockets and forward each document's messages to the one document server that owns it, chosen by consistent hashing on the document id. That server orders, transforms, logs and broadcasts operations. A compactor folds old operations into snapshots.

High-level design

Every operation on a document must be put in one order, and the cheapest place to do that is the memory of one process. So each open document has exactly one owner: a document server holding its text, recent operations and the people in it.

Gateways send every editor of a document to the same document server, picked by consistent hashing. That server orders operations and logs each one before acknowledging it.Editors' browsers and Gateways (WebSocket); Gateways to Membership store (ring map); Gateways and Document servers (by doc id); Document servers to Docs metadata (access, rev); Document servers to Snapshots (load); Document servers to Operation log (append op); Compactor to Operation log (read ops); Compactor to Snapshots (write).Editors'browsersGatewaysMembership storeDocument serversDocs metadataSnapshotsOperation logCompactorWebSocketring mapby doc idaccess, revloadappend opread opswrite
Gateways send every editor of a document to the same document server, picked by consistent hashing. That server orders operations and logs each one before acknowledging it.
Two edits at once, then the owner diesUse the arrows or ← → keys
  1. Two edits at once, then the owner dies, step 1Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server Bowns doc 42Doc server COperation log
    server rev
    1042

    1Document 42 is at revision 1042 and hashes to server B, so the gateways send Ana's and Ben's messages there.

  2. Two edits at once, then the owner dies, step 2Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BDoc server COperation log
    server rev
    1042
    Ana in flight
    seq 17
    Ben in flight
    seq 9

    2Ana and Ben type in the same 100 ms. Each sees their own change at once and sends it with base_rev 1042.

  3. Two edits at once, then the owner dies, step 3Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BDoc server COperation logrev 1043
    server rev
    1043
    Ana in flight
    seq 17
    Ben in flight
    seq 9

    3Ben's arrives first. It is based on the latest revision, so B logs it unchanged as 1043, on the shard leader and its replica.

  4. Two edits at once, then the owner dies, step 4Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BDoc server COperation log
    server rev
    1043
    Ana in flight
    seq 17
    Ben in flight
    none

    4Only now does B ack Ben and send the operation to Ana. An ack means the edit is durable.

  5. Two edits at once, then the owner dies, step 5Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BDoc server COperation logrev 1044
    server rev
    1044
    Ana in flight
    none
    Ben in flight
    none

    5Ana's arrives, still based on 1042. B transforms it against 1043 (shown in the next section), logs it as 1044, acks Ana and sends it to Ben. All copies match.

  6. Two edits at once, then the owner dies, step 6Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BdownDoc server COperation log
    server rev
    1044
    Ben in flight
    seq 10

    6Ben types again (seq 10). Server B's machine hangs. Ben has no ack, so his client keeps the operation.

  7. Two edits at once, then the owner dies, step 7Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BdownDoc server Cowns doc 42Operation log
    server rev
    1044
    Ben in flight
    seq 10

    7B's heartbeats stop, so the membership store drops it and the ring maps document 42 to C. Clients reconnect after a random wait with since=1044.

  8. Two edits at once, then the owner dies, step 8Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BdownDoc server COperation logrev 1045 (C)
    server rev
    1045
    Ben in flight
    seq 10

    8C loads the snapshot at 1000 plus the 44 operations after it, which also tell it each client's last seq. Ben resends seq 10; it is new, so C logs it as 1045.

  9. Two edits at once, then the owner dies, step 9Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server Bstale ownerDoc server COperation log1045 exists
    server rev
    1045

    9B was only paused. It wakes, still thinks it owns document 42, and tries to log a late operation as 1045. The insert fails: 1045 exists.

  10. Two edits at once, then the owner dies, step 10Ana and Gateways; Ben and Gateways; Gateways and Doc server B; Gateways and Doc server C; Doc server B to Operation log; Doc server C to Operation log.AnaBenGatewaysDoc server BDoc server COperation log
    server rev
    1045
    Ben in flight
    none

    10The failure means another server wrote 1045. B checks the ring, sees C owns document 42, drops it and closes its sockets; those clients retry on C. The log never forks.

1 / 10

Deep dives

Merging concurrent edits: OT or CRDT

The document is dog. Ana adds s at the end: insert at 3. At the same moment Ben adds my at the start: insert at 0. If each side applies the other's operation as sent, Ana gets my dogs, but Ben applies "insert s at 3" to my dog and gets my sdog. The copies have diverged.

Operational transformation (OT) fixes the position before applying. For two inserts: if the other insert landed before yours, shift yours right by its length; at the same position, the one the server committed first goes left. Deletes need matching rules (overlapping deletes shrink each other), and that is where implementations get subtle.

OT with a central server: dog, two editsUse the arrows or ← → keys
transform(mine, theirs):  if theirs.at < mine.at: mine.at += len(theirs.text)  if theirs.at == mine.at and theirs was committed first:    mine.at += len(theirs.text)  return mine
  1. Ana
    0d
    1o
    2g
    server
    0d
    1o
    2g
    Ben
    0d
    1o
    2g
    rev
    7

    1Everyone has dog at revision 7.

  2. Ana
    0d
    1o
    2g
    3s
    server
    0d
    1o
    2g
    Ben
    0m
    1y
    2
    3d
    4o
    5g
    rev
    7
    Ana sends
    ins 's' at 3
    Ben sends
    ins 'my ' at 0

    2Both type at once. Each sends an insert based on revision 7.

  3. Ana
    0d
    1o
    2g
    3s
    server
    0m
    1y
    2
    3d
    4o
    5g
    Ben
    0m
    1y
    2
    3d
    4o
    5g
    rev
    8

    3Ben's arrives first and is based on the latest revision, so it is stored unchanged as revision 8.

  4. Ana
    0d
    1o
    2g
    3s
    server
    0m
    1y
    2
    3d
    4o
    5g
    6s
    Ben
    0m
    1y
    2
    3d
    4o
    5g
    rev
    9
    stored as rev 9
    ins 's' at 6

    4Ana's arrives, based on 7. Revision 8 inserted 3 characters at 0, before 3, so the server shifts Ana's insert to 3 + 3 = 6.

  5. Ana
    0m
    1y
    2
    3d
    4o
    5g
    6s
    server
    0m
    1y
    2
    3d
    4o
    5g
    6s
    Ben
    0m
    1y
    2
    3d
    4o
    5g
    rev
    9
    Ana's pending
    now at 6

    5Ana's client gets revision 8 while her insert is unacknowledged. Her pending insert at 3 is after 0, so Ben's op applies as sent, and her pending op moves to 6.

  6. Ana
    0m
    1y
    2
    3d
    4o
    5g
    6s
    server
    0m
    1y
    2
    3d
    4o
    5g
    6s
    Ben
    0m
    1y
    2
    3d
    4o
    5g
    6s
    rev
    9
    converged
    yes

    6Ben receives revision 9, already transformed: insert at 6. Ana gets her ack. Three copies, one text: my dogs.

1 / 6

The central server is what keeps OT manageable. Because it picks one order, the transform only has to guarantee that A then B-transformed gives the same text as B then A-transformed. Without a server, peers see operations in different orders and the transform must meet a much harder second condition, which several published peer-to-peer OT algorithms were later shown to break.

A CRDT (conflict-free replicated data type) avoids positions. Every character gets a permanent unique id, such as (ben, 1), and an insert says "after the character with id X". Ana's s goes after the id of g; Ben's m goes at the start. Ids never shift, so concurrent operations can be applied in either order, a repeated one changes nothing, and every copy ends at my dogs. Two inserts after the same character are ordered by comparing their ids. A deleted character stays as a hidden tombstone, because a later insert may point at it.

Merging concurrent edits
OptionGood atBad atPick it when
OT with a central server Usual defaultSmall operations and a plain-text document. One place for order, access checks and history.Needs an owner online per document. Rich-text transforms are hard to get right; long offline sessions transform against many operations.A hosted product where every document already has a server. My choice here.
CRDTNo ordering server: edits merge in any order, so peer-to-peer and long offline work come naturally.Ids on every character and tombstones add metadata until compacted. Concurrent inserts can interleave in surprising ways.Offline-first or peer-to-peer apps, or several regions writing one document.

My pick is OT with a central server: the design needs one owner per document anyway, for order, presence and access checks, so OT's main cost is already paid. "Offline for days" or "no server in the middle" would flip it to a CRDT, with a server kept only to store and relay operations.

One server per document

The gateways hash the document id onto a consistent-hash ring of document servers. Ring membership lives in a small strongly consistent store that every gateway caches and watches. With hash mod N, adding a 41st server would remap almost every document. With a ring, about 1/41 move: 1,000,000 ÷ 41 ≈ 24,000 documents, each reloaded from its snapshot.

Getting every editor of a document to one place
OptionGood atBad atPick it when
Any server, order in the databaseNo ownership; any server takes any edit and relays it through pub/sub.Every operation needs a database round trip to claim a revision, and racing claims retry.Low edit rates, such as a shared to-do list.
Owner by consistent hashing Usual defaultOrdering happens in one process's memory, with no lookup per connection.Membership changes move documents; you cannot move one hot document on purpose.Many small documents. The usual answer.
Owner directoryYou can place a busy document on a quiet server.A lookup per open, and the directory must stay highly available.Very uneven load.

Ownership can briefly overlap: a server paused by a long garbage-collection run may wake still believing it owns a document. A lease makes this rare: an owner stops serving when its lease lapses, before the store reassigns the document. The log as a fence makes it harmless. An owner only inserts the revision after the last one it has seen, and each revision can be written once, so two servers can never both commit 1045. The loser of that race re-reads the log, and steps down if the ring names another owner. A new owner learns each client's last seq from the logged operations, so resends stay harmless. Consensus and leader election explains why a lease alone is not enough.

Storing operations and compacting them

The compactor writes a snapshot every 1,000 operations, so nobody replays a busy document from revision 1. Opening a document reads one 50 KB snapshot and at most 999 operations, about 150 KB. Keep fine-grained operations for 30 days (120 TB) for catch-up and recent history, then delete them. Do not keep every snapshot: that would be 40 million a day, 2 TB. Keep the newest, plus one per editing session for version history. A snapshot is a cache of the log, so a bad one is rebuilt from the previous snapshot and the operations.

Cursors, reconnects and offline work

Cursors are not operations. Nobody needs their history and losing one is harmless, so they stay in the owner's memory and are never logged. A cursor is sent with its revision; when Ben inserts 3 characters before Ana's cursor, it shifts right by 3 with the same transform used for text. Cursor traffic is what hurts big documents: 100 editors sending their cursor 10 times a second to 99 others is 100 × 10 × 99 = 99,000 messages a second. Instead the owner sends each client one combined update every 500 ms: 100 × 2 = 200 messages a second.

Reconnecting reuses the edit path. The client keeps its last acknowledged revision and its unacknowledged operations, reconnects with since=1044, transforms its pending operations against what it receives, and resends them with their original (client, seq), so nothing is applied twice.

Offline editing is a long reconnect: the offline edits are sent as one operation with the old base_rev and transformed against everything since. If that revision is older than the 30-day log, the server answers reload; the client fetches a snapshot and does a three-way text merge, or saves its version as a copy.

Bottlenecks and failures

At 10x (10 million operations a second), gateways and document servers scale by adding machines, and the ring moves a small share of documents each time. The log grows to about 500 shards. The real limit is one document, since one owner serves all its editors. At 100 editors, 100 × 0.5 = 50 operations a second sent to 99 others is about 5,000 messages a second: easy. A document read by 10,000 people needs a read-only stream for viewers through a separate fan-out tier.

When this diesWhat happensWhat you do
A document serverWith 40 servers, its 1,000,000 ÷ 40 = 25,000 documents lose their owner. Typing continues locally.The ring reassigns them; their 50,000 editors reconnect with jitter (over 10 seconds, 5,000 a second) and resend.
A gatewayIts sockets drop.Clients reconnect through another gateway and catch up; owners are unchanged.
A log shard leaderIts documents cannot commit; edits wait unacknowledged.The synchronous replica takes over with every acked operation; clients resend the rest.
The membership storeNo ownership changes.Gateways route on their cached ring; run it on 3 or 5 nodes.
The compactorNo new snapshots; opening gets slowly slower.Nothing is lost; restart it.

What a strong answer sounds like

LevelWhat the answer does
JuniorUses WebSockets and saves the document, notices that concurrent saves overwrite each other, and with prompting suggests locking or merging.
MidSends operations, explains OT or CRDTs with an example, and keeps an operation log with snapshots.
SeniorTies the merge choice to the architecture, routes editors with consistent hashing, fences stale owners, keeps cursors out of the log, and says what offline work and 100-editor documents cost.
Defending OTStrong answer
Interviewer

Everyone says CRDTs are the modern answer. Why OT?

You

I already have one owner per document for ordering, presence and access checks. With one server picking the order, OT only has to make pairs of operations agree, and the stored document stays plain text. A CRDT would remove that server, but I'd pay for ids on every character and tombstones. If offline for days or peer-to-peer were requirements, I'd switch.

Follow-up questions

How does undo work with several editors?

Undo only undoes your own edits. The client keeps the inverse of each of its operations, transforms it against everything committed since, and sends it as a new operation.

How do you add formatting such as bold?

Add operations over ranges, such as "bold from 10 to 18". The same transform shifts the range ends when text is inserted or deleted before or inside them.

Someone loses access while editing. What happens?

The metadata service tells the owner, which drops that user's socket and rejects their pending operations. Every operation passes through the owner, so there is one place to enforce it.

How would you run this in several regions?

Each document's owner lives in one region. Distant editors still type with zero delay, but see others' edits one cross-region round trip later. If two regions must accept writes for one document, that is the case for a CRDT.

How do you show last Tuesday's version?

Take the nearest snapshot before that time and, within 30 days, apply operations up to the chosen moment. Older versions are limited to the kept snapshots.

Self-review: collaborative editor0 of 12

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure