Two people type at the same spot at the same instant — how do both edits survive, and how does everyone end up staring at the exact same document? This chapter builds up the answer from naive saves that lose work to the two real techniques: operational transform and CRDTs.
Open a shared doc, put three people in it, and have them all type in the same paragraph. Somehow nobody's words get clobbered, and a second later all three screens show an identical document. That "somehow" is the entire subject of this chapter.
The naive instinct — "just save the document" — falls apart the moment two people edit at once, because one person's save quietly overwrites the other's. To make live collaboration actually work, we stop shipping whole documents and start shipping tiny edits ("insert x at position 5", "delete the character at 9"), and we give the system a rule for combining edits that arrive in any order so that everyone lands on the same final text.
There are two industrial-strength rules for doing that. Operational Transform (OT) reshapes each incoming edit so it still makes sense after the edits that raced ahead of it. CRDTs redesign the data itself so that edits simply merge, in any order, with no reshaping needed. We'll build intuition for both, prove to ourselves that they converge, then wire up transport, presence, and persistence around them.
A collaborative editor is a single document that several people change at the same time, live. Each keystroke from each person needs to show up on everyone else's screen within a moment, and — this is the hard part — when the dust settles, every screen must show exactly the same characters in exactly the same order. That property is called convergence.
What makes it hard is that edits race. Alice types an H at the start of a line while, at literally the same time, Bob types a W at the start of the same line. Neither saw the other's keystroke yet. Both edits are valid. Both must survive. And after they're combined, Alice's screen and Bob's screen must agree — it can't be that Alice ends up with HW… and Bob with WH….
So the real question of this chapter is narrow and sharp: two people type at the same spot at the same instant — how do both edits survive and everyone end up seeing the same document?
The obvious design is the one almost everyone reaches for first: each client keeps its own copy of the document, and when you make a change you send the whole document back to the server. The server keeps whichever copy arrived most recently. This is last-write-wins (LWW) on the whole document.
It works perfectly — for one person. The instant two people edit concurrently, it destroys work. Suppose the doc is "cat". Alice changes it to "cats" and saves. A half-second later Bob — who started from "cat" too and never saw Alice's s — changes his copy to "scat" and saves. Bob's save lands last, so the server now holds "scat". Alice's s is simply gone. No error, no conflict marker — her edit was overwritten wholesale.
The bug isn't that the server picked the wrong winner. The bug is that the unit of saving is the entire document. When you overwrite the whole thing, you can't keep two concurrent changes; one blob replaces the other. The fix is to shrink the unit: send operations — "insert s at position 3", "insert s at position 0" — and merge them at the level of individual characters. Both inserts can survive because they touch different things.
Here's the trouble with shipping raw operations: a position is only meaningful against a specific version of the text. Alice says "insert x at position 5." But before that op reaches Bob, Bob already inserted three characters near the front. Now Bob's document is longer and shifted — applying "insert at 5" literally would land in the wrong place. The op needs to be adjusted for the edits that happened concurrently.
That adjustment is operational transform. The core idea is a function — call it transform(a, b) — that takes two concurrent operations and rewrites one of them so it produces the right effect as if the other had already been applied. If Bob inserted ahead of Alice's target position, Alice's "insert at 5" becomes "insert at 8." Both edits survive; positions stay correct.
A central server orders the operations. Every client sends its ops to the server; the server picks an authoritative order, transforms each incoming op against the ops it has already accepted, applies it, and broadcasts the transformed op to everyone. Each client likewise transforms server ops against any local ops still in flight. This is the approach Google Docs historically used, and it's efficient — the only thing traveling over the wire is a small op plus a version number.
The catch: OT is famously tricky to get right. You need a correct transform function for every pair of operation types (insert vs insert, insert vs delete, delete vs delete…), and the transforms must satisfy algebraic properties so that no matter which order two clients apply things in, they land on the same text. Subtle bugs in those rules cause silent divergence, which is exactly the failure you were trying to prevent.
Doc → Doc, and transform is what makes those functions commute. OT manufactures commutativity by editing the operations; the next idea, CRDTs, bakes commutativity into the data so you never have to.OT keeps the data simple (it's just a string) and pays the price in clever transform logic. CRDTs — conflict-free replicated data types — flip the trade. They make the data structure cleverer so that concurrent edits merge deterministically no matter what order they arrive in, with no transform step and no central coordinator required.
The trick for text is to stop thinking of a document as "characters at positions 0, 1, 2…" — those positions shift every time someone inserts. Instead, give every character a permanent, globally unique id that also encodes where it belongs in the order. Two common schemes:
0.25 and 0.50, you pick 0.375. There's always room between any two fractions, so an insert never needs to renumber its neighbors.(timestamp, siteId) and a pointer to the character it was inserted after. Ordering the characters by these ids deterministically reconstructs the text on every replica.Once every character has a stable unique id, merging two replicas is almost embarrassingly simple: take the union of their characters (dedupe by id), then sort by id to read out the text. Two people inserting "at the same position" actually inserted characters with two different ids, so both survive, and the total order on ids decides their final sequence — identically on every machine. Deletes are handled by marking a character as a tombstone rather than removing it, so a delete and a concurrent insert can't disagree.
(1234, A)." Ids never move, so concurrent edits never fight over a slot — they just both exist, and the id order sorts them out.Both clients start from "cat". Client A inserts s at the front; client B inserts ! at the end — but the two ops reach the clients in opposite orders. In Naive LWW a whole-doc save wins and an edit vanishes (or the two ends disagree). Switch to OT / CRDT and both edits survive in the same order on both screens.
Strip away the implementation and OT and CRDTs are aiming at the identical target: "same operations, any order → same result." OT reaches it by transforming ops until applying them in any order produces the same text. CRDTs reach it by defining a merge that's commutative (order doesn't matter), associative (grouping doesn't matter), and idempotent (re-merging the same thing changes nothing).
Those three properties should ring a bell. A merge that's commutative, associative, and idempotent is exactly a join on a semilattice — the structure we met back in Chapter 7. The set of all possible document states forms a lattice, and merging two replicas is taking their least upper bound: the smallest state that contains everything both replicas have seen.
This is why CRDTs need no coordinator. Because merge is a join, it doesn't matter who merges with whom, in what order, or how many times. Replica A merging B's state, then C's, lands in the same place as C merging B then A. Convergence isn't something you carefully orchestrate — it's a mathematical consequence of the merge being a join. Monotonic progress up a lattice can't disagree with itself.
merge is its join (least upper bound). Commutative + associative + idempotent = order-, grouping-, and repeat-independent = guaranteed convergence with zero coordination. The algebra is the correctness proof.Each replica is a set of positioned characters (id, ch). Merge takes the union, drops duplicate ids, and sorts by id to read the text. Because union is commutative, associative, and idempotent, both replicas land on the same string — and merging the same state twice changes nothing. That's the join from §5, made concrete.
Convergence answers "what do we do with edits"; transport answers "how do edits get there fast." Collaboration is bidirectional and latency-sensitive — polling for changes would feel laggy — so edits travel over a persistent WebSocket connection (the full-duplex channel from Chapter 10). A client opens one socket to the server, streams its ops up as you type, and receives everyone else's ops streamed down, applying each as it arrives.
But a good collaborative editor shows more than text. You see the other people: their colored cursors, their selections, a little flag with their name, who's currently viewing. This layer is called presence or awareness, and it has different rules than the document itself.
Three editors share one line of text, each with a colored cursor broadcast over a simulated WebSocket. Hit Live cursors to watch them drift as their owners type. Then have Alice insert "ABC" at the start: notice Bob's and Carol's cursors jump three characters right, so each stays glued to the same character it was on.
The live session is in memory, but the document has to outlive it — survive a server restart, let a latecomer join and catch up, power version history and undo. The durable record is an append-only operation log: every accepted op, in order, written to storage. Replaying the whole log from the start reconstructs the document exactly, which doubles as your edit history and the basis for "see changes" and revert.
This is the same shape as event sourcing: the log is the source of truth, snapshots are a performance cache of folding that log, and the current document is just log.foldLeft(empty)(apply).
Both converge. The choice is about where you pay the cost and what shape your system has.
A rough rule of thumb: central server you already trust + bandwidth-sensitive → OT; offline-first, local-first, or peer-to-peer + willing to spend memory → CRDT. Here's the whole chapter as a checklist:
Here's the heart of §4 and §5 in code: a character is an id paired with a glyph, ids have a total order, and merge is "union the two replicas, drop duplicate ids, sort by id." That's it — and that one line gives you convergence with no central coordinator, because union-then-sort is commutative, associative, and idempotent (it's the join from Ch.7). Flip between the languages; the shape is identical.