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.
Collaborative editor / at a glance
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.
The scope question that matters is how strong the convergence guarantee has to be, and
almost nobody asks it.
You ask
They say
What 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.
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.
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.
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.
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.
op @ v412071which server?2what it missed3apply4append5broadcast v412086materialiseany past versioncursorEditor AEditor BGatewayholds the socketsCoordinatordoc to serverPresencecursors, ephemeralDocument serverone owner per docTransformagainst 41207..41260In memory docplus pending opsOp logappend only, durableSnapshotsevery few hundred opsHistory servicereads the log
op @ v412071which server?2what it missed3apply4append5broadcast v412086materialiseany past versioncursorEditor AEditor BGatewayholds the socketsCoordinatordoc to serverPresencecursors, ephemeralDocument serverone owner per docTransformagainst 41207..41260In memory docplus pending opsOp logappend only, durableSnapshotsevery few hundred opsHistory servicereads the log
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:
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.
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.
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.
It applies the result to the in memory document and assigns the next version number.
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.
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?
Here is the conflict from the opening paragraph, in slow motion. Both clients start from
the string hello world at version 7.
Client ADocument serverClient Binsert "X" at 0, base v7delete 1 char at 6, base v7apply A, now v8transform B's delete: 6 becomes 7apply A at 0, now v9apply B's delete at 7, now v9
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.
There is a second family of solutions that avoids transformation entirely by never using
positions.
Convergence strategy
123Op at position 6base v7Serverlinear historytransform()against v8..v12Op at position 9applied
123Op at position 6base v7Serverlinear historytransform()against v8..v12Op at position 9applied
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.
opsappendEditorsGatewayDocument serverone per docOp log
opsappendEditorsGatewayDocument serverone per docOp log
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.
Read every step as text
3 editors · 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.
50 editors · Degraded. A workshop. 250 operations a second in, and each one broadcast 49 times, so 12,000 messages a second out of one process. The transform queue is now genuinely doing work, because concurrent edits are constant rather than rare.
paste 200 pages · Down. Someone pastes an enormous block. As one operation it is a multi megabyte message broadcast to fifty people. Split large insertions into bounded operations on the client, and rate limit per client, or one paste stalls everybody on the document.
owner dies · Down. The document server crashes. Editing stops for this document until the lease expires and another server loads the snapshot and replays the tail. A few seconds of a frozen cursor, which users forgive, and no ambiguity about ordering, which they would not.
repaired · Healthy. Clients reconnect and resend pending operations with the same client id and sequence, so anything applied before the crash is recognised rather than duplicated. Idempotent operations are what make the recovery boring, which is the highest praise a recovery path can get.
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.
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.
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.