LearnHLDDesign a collaborative editor

Design a collaborative editor

Two people have the same document open. One types a character at position 4. At the same instant, the other deletes the word at position 2. Both edits are valid, both were made against the same version, and neither person did anything wrong.

There is no correct way to merge those two edits by sorting them or by timestamping them, because the second edit’s position number stopped being true the moment the first one was applied. That is the whole question, and everything else here is plumbing around it.

Step 1: Understand the problem

The scope question that matters is how strong the convergence guarantee has to be, and almost nobody asks it.

You askThey sayWhat it settles
How many people edit one document at once?Usually two or three. Occasionally fifty in a meeting.A server per document is affordable. Fifty editors on one document is a small fan out, which is a relief, because the hard part here is correctness and not scale.
Must every editor end up with an identical document?Always, no exceptions.The convergence requirement. Last write wins is off the table immediately, because two concurrent edits at different positions must both survive.
Does intent have to be preserved, or just the characters?Intent. If I bold a word while you move it, the word stays bold and moves.Operations described relative to the text rather than to positions in it. This is the answer that rules out the naive "send the whole document" design.
How fast does a keystroke have to appear locally?Instantly. Nobody tolerates latency on their own typing.Optimistic local application, with reconciliation afterwards. The client edits its own copy first and finds out later whether the server agreed.
Can someone edit offline?Yes, and it must merge when they reconnect.Operations have to compose after an arbitrary delay, not just a few hundred milliseconds. This makes the merge algorithm the centre of the design rather than an edge case.
Do we need history and undo?Full version history, and undo that only undoes my own edits.An append only operation log as the source of truth, with the document as a materialised view of it. Per user undo is much harder than it sounds and is worth flagging as such.

What you are building, and what you cut

In scope
  • Several people editing one text document at once. Every editor converges to the same result, always.
  • Local edits apply instantly. Reconciliation happens behind the cursor, never in front of it.
  • Presence and cursors. Cheap, ephemeral, and the thing users notice first.
  • History, and offline edits that merge on reconnect. The operation log gives you both.
The numbers you commit to
  • A keystroke is visible locally in under 16ms and to others in under 200ms.
  • Every client converges to a byte identical document.
  • A document survives any server restarting mid edit.
  • Fifty concurrent editors on one document without degradation.
Cut, and say so out loud
  • Rich media, tables and embedded objects. Same machinery, much messier operations, and it triples the page.
  • Access control and sharing. Real and orthogonal, and it belongs one layer up.
  • Rendering and layout. That is a word processor, not a distributed system.
  • Spreadsheet formula recalculation, which is a dependency graph problem hiding behind the same UI.

Back of the envelope

Traffic per document
5
60 wpm
500 thousand
Operations per editor60 wpm x 5 chars / 60 = 5.0 per second
Operations per document5.0 x 5 = 25 per second
Messages fanned out per document25 x 4 = 100 per second
Total operations across the fleet12,500,000 per second
Open connections500k docs x 5 = 2,500,000
25 ops/sec
per document, which is nothing, and that is the point

Per document this is a trivial workload. 25 operations a second means one process can comfortably own a document and serialise every edit to it, which removes the entire distributed consensus problem. The scale challenge is 2,500,000 open connections, and connections are a known problem with known answers. The hard problem is that two of those 25 operations conflict.

The trap in these numbers

The low per document rate tempts people into batching keystrokes to reduce traffic. Resist it, or at least be careful: batching changes what an operation means, and two batched operations conflict in ways two single character operations do not. Debounce the network send by a few tens of milliseconds if you must, but keep operations small and let the transform handle them. Cleverness in the merge algorithm is where bugs live forever.

Step 2: Propose the high level design

The API

GET/v1/docs/{id}/session
returns 200 { snapshot, version: 41207, wsUrl, sessionId }
Why: One request that returns the document state and the version it corresponds to. Everything after this is relative to that version number, so getting the snapshot and the version atomically is essential. Fetch them separately and you have a race that corrupts documents.
POSTover the socket: op
{
  "docId": "d_91",
  "baseVersion": 41207,
  "clientId": "c_7f",
  "seq": 88,
  "op": { "type": "insert", "pos": 4, "text": "h" }
}
returns ack { version: 41208 }
Why: Every operation states which version it was made against. The server needs that to know what to transform against, and the client sequence number makes retries safe: a resent operation with the same clientId and seq is recognised rather than applied twice.
GET/v1/docs/{id}/ops?since=41207
returns 200 { ops: [...], version: 41260 }
Why: How a client that was offline catches up. It replays operations since its last known version rather than downloading the document, which preserves its own pending local edits and lets them be transformed against what it missed.
GET/v1/docs/{id}/history?at=2026-09-01T10:00:00Z
returns 200 { snapshot, version }
Why: Version history falls out of the operation log for free. This is worth saying out loud in an interview, because it turns a feature request into a property of a decision you already made.

The data model

document_opsappend only log per document, plus periodic snapshots
doc_iduuidPKPartition key. One document is one log, read and written by one owner at a time.
versionbigintPKClustering key, strictly increasing, assigned by the document owner. This ordering is the total order every client agrees on.
client_idvarchar(32)UQWith seq, makes an operation idempotent. A reconnecting client resends and the server recognises it.
opjsoninsert, delete or format, with positions relative to the base version. Small, and it is never rewritten after being logged.
base_versionbigintWhat the client thought the document was. The gap between this and current version is exactly what has to be transformed against.
author_idbigintIDXFor per user undo, and for showing who wrote what.
Sample row
d_91 | 41208 | c_7f | {insert, pos 4, "h"} | 41207 | 4471
Snapshots are a cache, not the truth. Every few hundred operations, write the materialised document so a new client does not replay from the beginning. Delete the snapshots and everything still works, only slower, which is the property that tells you the log is genuinely the source of truth.

The whole system on one whiteboard

Figure 1. One process owns one document. That single constraint turns a distributed consistency problem into a local ordering problem, and it is the most important box on the page.

Walking Figure 1:

  1. An editor sends an operation tagged with the version it was written against. Locally it has already been applied, because nobody waits for a server to see their own keystroke.
  2. The gateway forwards it to the one server that currently owns this document. The coordinator is what makes “the one server” true, and it is the box that turns this from a consensus problem into an ordering problem.
  3. The document server transforms the operation against everything that has been applied since the client’s base version. Usually that is nothing and the transform is a no op.
  4. It applies the result to the in memory document and assigns the next version number.
  5. It appends to the durable log, and only then acknowledges. Acknowledging before the append means a server crash can lose an edit the user watched appear.
  6. The transformed operation goes back through the gateway to everyone else on the document, and each client applies it, transforming against its own pending operations that the server has not seen yet.

Cursors and presence go around all of this. They are ephemeral, they are lost on disconnect without consequence, and putting them through the operation log would multiply your durable write rate by ten for data that is worthless in a second.

Your answer

Two clients both send an insert based on version 41207. The server applies one and assigns 41208. What does it have to do to the second one before applying it, and what happens if it skips that step?

Step 3: Design deep dive

Why positions lie

Here is the conflict from the opening paragraph, in slow motion. Both clients start from the string hello world at version 7.

1/6 A types X at the very start. Locally A now sees "Xhello world" and its cursor is at 1. A does not wait for the server.
Figure 2. Without transformation the two documents differ by one character forever. The transform is a small function that rewrites the second operation to mean what its author intended.

This is operational transformation. One function, transform(opA, opB), that rewrites an operation so it means the same thing after another operation has been applied. It is easy to describe and famously easy to get wrong. Insert against insert at the same position needs a tiebreak. Delete against delete of the same character must not delete twice. Every pair of operation types needs its own case, and missing one corrupts documents months later.

Why the central server is not a shortcut

Operational transformation with a single serialisation point needs one transform function against a linear history. Peer to peer OT, where any client can transform against any other, needs the transform to satisfy properties that are genuinely hard to prove and that several published algorithms got wrong for years. Assigning one owner per document is not laziness, it is what makes the correctness argument tractable, and it is exactly what Google Docs does.

The other answer, and when to pick it

There is a second family of solutions that avoids transformation entirely by never using positions.

Convergence strategy
Operations carry positions and get rewritten as they pass each other. Documents stay small, because a delete really deletes. The cost is a transform function with a case for every pair of operation types, and a hard requirement that one place assigns the order. Google Docs works this way, and so does almost every editor with a server in the middle.

Pick operational transformation for a server based document editor and say why in one sentence: you already have a natural serialisation point, so you may as well use it and avoid paying tombstone overhead forever. Pick a CRDT when peers must merge without a server, which is the offline first and peer to peer case.

Then name the thing that makes the answer credible: garbage collecting tombstones in a CRDT requires knowing that every replica has seen a deletion, and if replicas can be offline indefinitely, you cannot know that. That is the practical reason CRDT documents grow, and it is a much better answer than “CRDTs use more memory”.

Owning a document, and what happens when the owner dies

One server per document is the assumption everything rests on, so it needs a real answer for failure.

The coordinator maintains a lease: server 12 owns document d_91 for the next thirty seconds, renewed continuously. Gateways route by asking the coordinator, and cache the answer for the lease duration. When server 12 dies, the lease expires, and only then may another server claim the document. The gap is deliberate: two servers assigning version numbers to one document is the one failure this design cannot tolerate, so it prefers a few seconds of unavailability to a moment of ambiguity.

Recovery is straightforward because the log is the truth. The new owner loads the most recent snapshot, replays operations after it, and announces the current version. Clients reconnect, send their pending operations again with the same client id and sequence number, and the server recognises anything it had already applied before it crashed.

The follow up you will get

“What if a client was offline for a week?” It reconnects with base version 41207 while the document is at 89000. Transforming its pending operations against 48,000 later ones is expensive and, worse, the result is often nonsense: the paragraph it was editing may no longer exist. The honest answer is that past some threshold you stop merging and start branching. Show the user their version as a suggestion or a copy, and let a human decide. Every editor that survives contact with real users has this escape hatch.

Break it

Editing pressure
3 editors
3 editors50 editorspaste 200 pagesowner diesrepaired
Healthy. Fifteen operations a second on one document. The transform is almost always a no op because nothing arrived between the client reading and writing. This is the normal case and it is trivial.

Notice that none of the failures above are about scale. A collaborative editor fails on correctness under concurrency and on one enormous operation, and both of those happen at five users just as readily as at fifty.

Trade-offs

ChoiceWhat you gainWhat you payPick it when
One server owns one documentA linear order for free, which makes the transform tractable and provably correct.A few seconds of unavailability when that server dies, and a coordinator to run.Any collaborative system with a server in the middle. The alternative is peer to peer OT, which is genuinely hard to get right.
Operational transformation over CRDTNo per character metadata and no tombstones, so a document edited for a decade stays small.A transform function with a case per operation pair, and it must run somewhere central.Server based editors. Choose a CRDT when peers must merge with no server, or when offline is the normal state rather than the exception.
Optimistic local applicationTyping is instant, which is not negotiable in an editor.The client holds pending operations and must transform incoming ones against them, so the algorithm lives on both sides.Always for text input. The alternative is a keystroke that waits for a round trip, which no user tolerates.
Log as truth, snapshots as cacheHistory, undo and crash recovery all fall out of one decision.Replay cost grows between snapshots, and the log grows forever unless compacted.Any system where the sequence of changes is as interesting as the current state, which for documents it always is.

Interview replay

Interviewer
Two people are typing in the same paragraph. What happens?
Opening, and it goes straight to the only hard part.
You
Both apply their own keystroke locally and immediately, then send it to the server tagged with the version they were editing against. One server owns the document, so it picks an order, applies the first, and transforms the second against it before applying. Transforming means rewriting positions: if the first operation inserted a character before your edit point, your position shifts by one. Then it broadcasts the transformed operation, and each client transforms it again against whatever it has pending locally.
Names transformation and the reason for it in one answer, with a concrete example of what a transform actually does.
Interviewer
Why do you need one server per document? Could you not shard it?
Probing whether the single owner was a considered choice.
You
The transform is defined against a linear history. If two servers assign version numbers to the same document there is no single history to transform against, and you are in peer to peer OT, which has to satisfy convergence properties that several published algorithms got wrong. A document is a few hundred operations a second at worst, so one process handles it easily. I would rather spend a lease and a coordinator than a correctness proof.
The phrase "I would rather spend a lease than a correctness proof" is the kind of trade a senior engineer actually makes.
Interviewer
What about CRDTs? I hear they solve this without a server.
Checking whether you know the alternative or only the buzzword.
You
They do, by giving every character an id so operations say "insert after this character" instead of "insert at position six". Positions never shift, so operations commute and order stops mattering. The cost is tombstones: deletions have to be remembered so late arriving operations can still be positioned, and you can only garbage collect a tombstone when every replica has seen the deletion, which you cannot know if replicas go offline indefinitely. For a server based editor I would take OT and keep documents small. For an offline first or peer to peer tool I would take the CRDT and accept the metadata.
Explains the mechanism, then gives the precise reason tombstones cannot be collected. That detail is what distinguishes reading about CRDTs from considering them.
Interviewer
The document server crashes with operations in flight.
Recovery. They want to know whether the log came before the ack.
You
Anything acknowledged was already appended to the log, because we append before we ack, so nothing a user watched appear is lost. In flight operations are resent by clients with the same client id and sequence number, and the new owner recognises duplicates. Recovery is loading the latest snapshot and replaying the tail. The visible cost is a few seconds of frozen cursors while the lease expires, which I would take over the alternative of two servers ordering the same document.
States the ordering of append and ack explicitly. That single detail is the difference between losing edits and not.
Interviewer
Someone edits offline for a week and reconnects.
The question with no clean answer. They want to see whether you invent one.
You
Mechanically I can transform their pending operations against everything they missed, but I do not think that produces a good result, and I would say so. Their edits were about a paragraph that may not exist any more, so a technically correct merge can produce text nobody wrote. Past some threshold, and I do not know exactly where it is, I would stop merging and present their version as a branch for a human to reconcile. I would want to look at real reconnect data before picking that threshold.
Recognises that the correct algorithm produces the wrong product, and admits not knowing the threshold. Both are rare and both score.

Checkpoint

Checkpoint

1. Why can concurrent edits not simply be ordered by timestamp and applied?

2. What does a CRDT trade away to avoid needing a central server?

3. Same design, but for a spreadsheet where cells contain formulas. What is the first new problem?

Worth memorising
  • 60 words a minute is about 5 operations a second per editor. Per document this is a trivial workload.
  • One server owns one document. That is what makes the transform provably correct against a linear history.
  • Append to the log before acknowledging. Reverse it and you lose edits the user watched appear.
  • OT keeps documents small, CRDTs remove the server and pay in tombstones. Pick by whether you have a server.
Say this in 60 seconds

A collaborative editor is a convergence problem wearing a UI. The core issue is that an operation says insert at position four, and the moment another edit lands before that point, position four means something different. So every operation carries the version it was written against, one server owns each document and assigns a linear order, and it transforms each incoming operation against everything applied since that base version before applying it. Clients apply their own keystrokes immediately and transform inbound operations against their own pending ones, because nobody accepts latency on their own typing. The log is the source of truth and the document is a materialised view of it, which gives history, undo and crash recovery from one decision, with snapshots as a cache. Single ownership is the key constraint: it makes the transform provably correct against a linear history, and I would rather pay a lease and a coordinator than attempt peer to peer transformation. The alternative family is CRDTs, which give every character an id so operations commute with no server, and pay for it in tombstones that cannot be collected while any replica might still be offline. For a server based editor I would take operational transformation.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.