Courses › System Design › Patterns
Merging Concurrent Edits
OT vs CRDTs, per-document session servers, operation logs, snapshots and presence.
Google Docs, Figma, Notion, shared whiteboards: several people edit the same document at the same time, everyone sees everyone else's changes within a fraction of a second, and edits never silently disappear. The hard part isn't sending updates — it's merging concurrent edits so every copy ends up identical and still matches what each person meant.
Why naive approaches fail
- Lock the document — only one editor at a time. Correct, but it isn't collaboration.
- Last write wins on the whole document — two people type at once, one person's work vanishes.
- Send positional edits as-is — they go wrong as soon as edits are concurrent:
document: "cat"
Alice: insert "s" at position 3 → "cats"
Bob: delete position 0 → "at"
Alice applies Bob's delete: "ats" ✓
Bob applies Alice's insert at 3: "at" + ? → position 3 no longer exists ✗Each edit was written against a version of the document that the other person no longer has. You need a way to adjust or design operations so they can be applied in any order.
Operational Transformation (OT)
Operations (insert / delete at a position) are transformed against operations that happened concurrently before they're applied. In the example, when Bob receives Alice's "insert at 3", the system knows Bob already deleted a character before position 3, so it shifts the insert to position 2.
- A central server assigns a single order to all operations; each client transforms its pending local operations against the ones the server accepted first.
- Clients apply their own edits immediately (optimistic UI), then reconcile when the server's order arrives.
- Google Docs is the classic OT system.
Trade-offs: proven and memory-efficient, but the transformation rules are notoriously hard to get right, and it relies on a central server.
CRDTs
Conflict-free Replicated Data Types build the merge rules into the data structure, so any two replicas that have seen the same set of operations converge — in any order, without a central server.
- For text, each character gets a unique, stable ID (e.g. site ID + counter) and a position defined relative to its neighbours rather than as an index. Deletes leave tombstones.
- Libraries like Yjs and Automerge provide CRDT text, lists and maps that you sync over any transport.
Trade-offs: great for offline editing and peer-to-peer sync; costs more memory (per-character metadata, tombstones) and needs periodic compaction.
[!NOTE] Not every editor needs per-character merging Figma edits objects with properties (a rectangle's x, y, color), not a stream of characters. It uses a server-authoritative model with last-writer-wins per property: if two people change the same rectangle's color, one wins; if one changes the color and the other moves it, both changes survive. Pick the simplest merge rule your data allows.
The architecture
a = client "Editor A" @0,0.4
b = client "Editor B" @0,1.6
gw = service "WebSocket\nGateway" @1,1
doc = service "Document Session\n(one owner per doc)" @2,1
log = store "Operation Log" @3,0.4
snap = blob "Snapshots" @3,1.6
a <-> gw
b <-> gw
gw <-> doc : ops, presence
doc -> log : append
doc -> snap : every N ops- Persistent connections (WebSockets) carry operations both ways with low latency (see Real-time Updates).
- One session owner per document. Route all editors of a document to the same server process (consistent hashing on document ID), which holds the document in memory, orders operations and broadcasts them. If it dies, another server loads the latest snapshot plus the log and takes over.
- An append-only operation log is the source of truth — every accepted operation with its version — and snapshots every few hundred operations, so loading a document doesn't replay its whole history. The log also gives you version history and undo.
- Clients reconnect and catch up by sending the last version they saw and receiving every operation after it.
Presence and cursors
Cursors, selections and "Alice is viewing" are ephemeral: send them over the same connection, throttle them (10–20 updates a second at most), never persist them, and let them expire if a client goes quiet.
Scaling
- Most documents have a handful of editors, so sessions are cheap and spread across many servers by document ID.
- Large audiences (a doc viewed by 5,000 people during an all-hands) split into a few editors and many viewers; viewers can get batched updates or a periodically refreshed snapshot.
- Cap document size and the operation rate per client, so one runaway script can't overwhelm a session.
Permissions and offline
Check edit permission when an operation is accepted by the server, not just when the document is opened — access can be revoked mid-session. For offline edits, queue operations locally and sync on reconnect; CRDTs merge these naturally, and OT systems transform the queued operations against everything that happened meanwhile.
[!INTERVIEW] What a strong answer sounds like "Each document has one session owner, chosen by consistent hashing on doc ID, which holds the doc in memory. Clients apply edits locally and send operations with the version they were based on; the server transforms them against anything newer — OT — assigns the next version, appends to the operation log and broadcasts. We snapshot every 500 operations. On reconnect a client sends its last version and replays what it missed. Cursors go over the same socket but are throttled and never stored."