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. Now both screens have to
end up showing exactly the same text. Watch one edit make the trip.
Type. A types a letter and sees it at once. The edit goes out tagged with the version A was looking at: insert "h" at position 4, made against version 41207.
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.
A few words come up on every line of this page:
An operation (op for short) is one small edit: insert "h" at position 4, or delete one
character at position 6. Editors send operations, never the whole document.
A version is a counter on the document. Every applied operation bumps it by one, so
two people at version 41207 are looking at exactly the same text.
Converge means every screen ends up with exactly the same text, byte for byte.
To transform an operation is to rewrite its position so it still points at the spot its
author meant, after other edits have landed in front of it. This is operational
transformation, or OT, and it is the heart of the page.
The real Google Docs
This page builds a live collaborative editor like Google Docs: several people typing in one document and seeing each other's changes as they happen. The numbers below are interview assumptions, not Google's own. The core choice does match the real product: Google Docs merges edits made at the same time with operational transformation, the technique this page picks.
The scope question that matters is how strong the convergence guarantee has to be, and
almost nobody asks it. Each answer below settles a line in the design.
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 (each edit copied out to 49 other people), 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: assume the edit will be fine, show it now, and fix things up 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.
A worked number first. A word is about five characters, so someone typing 60 words a minute
produces 300 characters a minute, which is 5 keystrokes, and so 5 operations, a second. Move
the sliders to see what that means for one document and for the whole fleet. (The method is in
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 (put them in one line, one at a time), 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 (wait a few tens of milliseconds, then send the small operations that built up in one message) if you must, but keep operations small
and let the transform handle them. Cleverness in the merge algorithm is where bugs live
forever.
Opening a document is a normal HTTP request. After that the editor holds a WebSocket: one
connection that stays open, so the server can push other people's edits the instant they
happen instead of waiting to be asked. WebSockets vs SSE vs long
polling compares the options. Every edit then
travels over that socket, and plain HTTP is left for catching up and for history.
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. That is the same trick as an idempotency key.
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 document is not stored as text that gets overwritten. It is stored as the list of every
operation ever applied, in order, which is called an append only log: new rows are only
ever added at the end, never changed. The text you see is rebuilt from that list, so it is a
materialised view, a result worked out from the real data and kept around for speed.
document_opsappend only log per document, plus periodic snapshots
doc_id
uuid
PK
Partition key. One document is one log, read and written by one owner at a time.
version
bigint
PK
Clustering key, strictly increasing, assigned by the document owner. This ordering is the total order every client agrees on.
client_id
varchar(32)
UQ
With seq, makes an operation idempotent: applying it twice has the same effect as once. A reconnecting client resends and the server recognises it.
seq
int
UQ
The client counts its own operations 1, 2, 3. The pair of client_id and seq is unique, which is how a resent operation is spotted.
op
json
insert, delete or format, with positions relative to the base version. Small, and it is never rewritten after being logged.
base_version
bigint
What the client thought the document was. The gap between this and current version is exactly what has to be transformed against.
author_id
bigint
IDX
For per user undo, and for showing who wrote what.
Snapshots are a cache, not the truth. A snapshot is the finished text at one version. Every few hundred operations, write one 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.
Figure 1 is the picture at the top of the page, drawn flat, with the numbered path of one edit.
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 holds the open sockets and forwards the operation to the one server that
currently owns this document. The coordinator is a small service that keeps the map
of which server owns which document. It is what makes "the one server" true, and it turns
a consensus problem (many machines agreeing on an order) into an ordering problem (one
machine picking an order).
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 (who is in the document, and where their cursor is) 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?
Two people edit the same document at once. How do both edits survive?
1
First try
Send the whole document, last save wins
Each editor sends its full copy of the text. The server keeps whichever arrived last. It takes an afternoon to build.
Where it breaks: A types a word in paragraph one while B fixes a typo in paragraph three. B saves a moment later, and A's word is gone, because B's copy never had it. The requirement was that both edits survive.
2
Better
Send small operations, apply them in arrival order
Send "insert X at 0" and "delete at 5" instead of the whole text. The server applies them in the order they arrive. Both edits survive now.
Where it breaks: B wrote "delete at 5" against the old text. Once A's insert lands in front, position 5 holds a different letter, so the wrong letter is deleted and the two screens drift apart. Ordering by timestamp has the same flaw: any order still applies a position that has gone stale.
3
What you ship
Tag each operation with its version, one server orders, and transform
Every operation says which version it was written against. One server owns the document and puts operations in a single line. Before applying one, it rewrites its position against everything applied since that version. Every screen converges.
Try the second rung yourself, then switch on the rewrite.
Two edits, one line
0 ("q")
5 ("␣")
Both start from "quick fox"
A's screenremoved k
Xquic␣fox
B's screenremoved ␣
Xquickfox
The two screens disagree
B meant to delete "␣". On A's screen the X had already pushed everything right, so position 5 now holds "k" and that is what got deleted. The two screens now differ, and nothing will ever fix it.
Each person sees their own edit first and the other edit second, which is what really happens when both type at once. "Rewrite positions" is the transform: move a position one to the right if an insert landed before it, one to the left if a delete did.
Here is the conflict from the opening paragraph, in slow motion, the way the server sees it.
Both clients start from the text hello world at version 7. Count from zero: h is
position 0, and the space is position 5.
Client ADocument serverClient Binsert "X" at 0, base v7delete 1 char at 5, base v7apply A, now v8transform B's delete: 5 becomes 6A's insert at 0 (v8)B's delete at 6 (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. For an insert
and a delete the rule is short: if the other edit inserted text in front of your position,
move one to the right; if it deleted text in front of your position, move one to the left. 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. A CRDT (conflict free replicated data type) is a data structure built so that
copies can apply the same edits in any order and still end up identical. For text, that means
giving every character its own permanent id. The CRDTs
chapter builds them from counters up.
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.
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. A tombstone is the marker a CRDT
keeps for a deleted character, so that a late edit saying "insert after that character" can
still find its place. 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.
Who decides the order of edits on one document?
1
First try
Any server accepts edits for any document
Spread edits across servers the way you spread web requests, so no server is special.
Where it breaks: Two servers each assign version 41208 to a different edit. There is no single history left to transform against, and you are now doing peer to peer OT, the version that several published algorithms got wrong.
2
Better
One fixed server per document
Hash the document id to pick its server. Every edit for d_91 goes to the same place, so there is one line of edits again.
Where it breaks: That server dies, or only looks dead because it paused or the network hiccuped. If another server starts ordering the document while the first is still running, two servers hand out version numbers again.
3
What you ship
An owner holds a lease, and the next owner waits for it to expire
The coordinator gives one server a lease: a claim on the document that runs out unless it is renewed. A new owner may only take over after the old lease has expired, then rebuilds the document from the log.
The coordinator maintains a lease: server 12 owns document d_91 for the next thirty
seconds, renewed continuously. (Leases are covered properly in leader election and
locks.) 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.
A lease alone does not cover one case: a server that froze, missed its renewals, and then
wakes up still believing it is the owner. The usual guard is a fencing token: each new
lease carries a higher number, and the log refuses appends that carry an older one. The
holder does not know it
lost shows why
nothing short of that check closes the gap.
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.
Resending is safe only because the server can spot repeats. Delivery
guarantees explains why
the order of "append, then acknowledge" decides whether a crash loses an edit or repeats it,
and idempotency keys shows the same
dedupe idea for payments.
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.