A system design interview guide to building a Google-Docs-style editor where many people type into the same document at the same time and every screen ends up showing exactly the same text.
Student edition · start with the guarantee
Explain how concurrent edits converge while separating a two-operation demo from a complete collaboration protocol.
Correctness contract: Compatible replicas converge on the same accepted operations under the chosen OT/CRDT protocol; identities, deletes, causality, authorization, and history retention are explicit.
Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.
Real-time collaborative editing looks like magic the first time you see two cursors moving in the same paragraph, but underneath it is one of the cleaner distributed-systems problems you will be asked to design. The single hard requirement is convergence: no matter who typed what, in what order, or on which flaky network, every copy of the document must end up identical. That sounds obvious until you notice that two people can edit the same character position at literally the same moment, and there is no global clock that says which one happened first. This guide builds up the design from that core problem, walks through the two dominant approaches (Operational Transformation and CRDTs), covers the sync architecture that ties clients together, and then handles the practical parts an interviewer will push on: cursors, offline edits, and the tradeoffs between the approaches.
Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.
Operational Transformation
OT adjusts an edit against concurrent edits so both can be applied to a common document state. A correct algorithm and its control protocol must preserve convergence and the intended editing semantics.
Example. Alice inserts X at index 1 of AB; Bob inserts Y at index 1. A deterministic site tie-break orders X before Y, and one transformed insertion moves to index 2.
Limit. A two-operation insert example is not a complete text OT algorithm. Multi-character deletes, pending-operation rebasing, duplicate IDs, acknowledgements, and server history require a reviewed implementation.
Conflict-free Replicated Data Types
Data types with merge rules designed to converge after replicas have received compatible updates. State-based CRDT merges are associative, commutative, and idempotent; operation-based designs impose their own delivery/causality rules.
Example. A grow-only set merges by union. A two-phase set tracks adds and removals; a removed ID stays absent even if an old add arrives later.
Limit. Not every CRDT is a sorted list of fractional positions. Deletions, stable identities, causal dependencies, offline replicas, and tombstone garbage collection matter. Convergence does not guarantee desirable text ordering.
Sequence numbers and replay cursors
A sequence number records committed order within a stream. A cursor is a consumer's durable position in that stream, so it can resume without skipping missing events. An opaque identity is not automatically such a position.
Example. A device receives message sequence 10 then 12. It acknowledges contiguous 10, buffers 12, and fetches 11 before advancing to 12.
Limit. A Snowflake timestamp can arrive late behind an already-acknowledged cursor. Use per-stream commit order or a durable inbox offset, define retention gaps, and use separate device cursors.
Idempotency keys
A client-generated identity for one logical operation. The server binds it to caller scope, endpoint, request fingerprint, state, and result. A retry uses the same key and reuses the original outcome rather than repeating the effect.
Example. Payment p42 times out after the provider charged it. Retrying with p42 lets the provider return the same charge instead of creating a new one.
Limit. A unique local claim is insufficient if the business effect and saved result can diverge. Keep unknown outcomes pending, use downstream idempotency, and state the retention/retry horizon.
Presence and failure detection
Presence is an estimate of recently observed connection activity. Heartbeats refresh session expiry, and a timeout suspects that a session is no longer live. A user may have several sessions/devices.
Example. A phone disconnects while its laptop keeps sending heartbeats. The user remains online if the product defines online as any active session.
Limit. Timeouts cannot distinguish a dead process from a paused or partitioned one. Counts should say sessions, users, or recently active viewers; privacy and stale snapshots need explicit policy.
Tombstones
A durable delete marker that carries a version. Replicas merge it like a write so an old live value is not treated as a new record during repair.
Example. A deletes key k while B is offline. A later repair sends the versioned delete to B, removing B's old value.
Limit. Collecting the marker too early can resurrect data. Safe retention depends on repair completion, replica return policies, and the maximum tolerated offline period.
1. Requirements and Clarifying Questions
What is being edited? Rich text documents [yes]; spreadsheets and drawings share the ideas but not the details.
How many editors per document? Usually 1–5; allow up to about 100 active editors, more as viewers.
Offline editing? Yes, with merge on reconnect.
Cursors and presence? Yes.
Version history? Yes, browse and restore earlier versions.
Functional
Non-functional
Several users edit one document at the same time.
Local typing feels instant (no round trip before showing a key).
Everyone sees others' edits and cursors.
Remote edits appear within ~100–500 ms.
Edits made offline merge on reconnect.
All copies converge to the same text; no edit is silently lost.
Version history and restore.
Durable: an acknowledged edit survives a server crash.
Intuition
Picture two people editing the same printed page with pencils, then mailing each other notes like "insert 'x' before the 5th letter". If Bob erased a letter near the start, Alice's "5th letter" is now Bob's 4th. OT fixes the note ("make it the 4th"). A CRDT gives every letter a permanent name so the note says "insert after letter #A17", which never goes stale.
2. Back-of-the-Envelope Estimation
Quantity
Working
Result
Concurrent editors at peak
assumption
1 M
Ops in
1 M × ~2 ops/s (keystrokes batched)
2 M ops/s
Ops out
fan-out to ~3 other clients per doc
6 M ops/s
Inbound bandwidth
2 M × ~50 B
≈ 100 MB/s
Outbound bandwidth
6 M × ~50 B
≈ 300 MB/s
Ops per day
30% of peak on average × 2 M × 86,400
≈ 5.2 × 1010
Op log growth
5.2 × 1010 × 50 B
≈ 2.6 TB/day
Two lessons. First, the op log grows by terabytes a day, so you must snapshot and compact: store a full snapshot every so often and keep only recent ops hot. Second, the load is spread over millions of small documents, each needing only a few ops per second. That is why one server can own each document: shard by doc_id, not by user. A single very popular document is the hard case, which is why products cap simultaneous editors and turn extra users into read-only viewers.
3. API and Data Model
GET /v1/docs/{doc_id} -> { snapshot, snapshot_seq } # open a doc
WS /v1/docs/{doc_id}/live # persistent connection
client -> server { "type": "op", "base_seq": 1041, "op": {...}, "client_op_id": "c7-18" }
server -> client { "type": "ack", "client_op_id": "c7-18", "seq": 1043 }
server -> client { "type": "op", "seq": 1044, "op": {...} }
both ways { "type": "presence", "cursor": 212, "selection": [212, 220] }
GET /v1/docs/{doc_id}/ops?since=1041 # catch up after reconnect
documents (doc_id PK, owner, title, created_at, acl)
op_log (doc_id, seq, author, client_op_id, op_blob, ts) PRIMARY KEY (doc_id, seq)
snapshots (doc_id, seq, content_blob) PRIMARY KEY (doc_id, seq)
-- open = latest snapshot + ops after snapshot.seq-- client_op_id makes a resent op idempotent
4. The Core Problem
The document is shared mutable state, and several clients hold their own copy of it. Each client edits its local copy instantly — you cannot wait for a network round trip before showing the user the letter they just typed, or the editor feels broken. That means edits happen optimistically and locally, and only afterwards travel to everyone else. The moment you allow that, you have admitted concurrency: two clients can make edits that neither has seen yet, and those edits can touch the same region of the document.
The trap is to think of an edit as "the new text." If Alice replaces the whole document with her version and Bob replaces it with his, the last write wins and one person's work silently vanishes. The fix is to represent each change as a small, position-aware operation — "insert the character X at position 5," "delete the character at position 0" — rather than a whole-document snapshot. Operations are tiny, they can be ordered and replayed, and they describe intent rather than result. But operations alone are not enough, because a position like "5" means different things on two copies that have already diverged. Resolving that mismatch is the entire subject of the next two sections.
A useful framing for an interview: you are not building a text editor, you are building a replicated data structure that must converge. Every design choice — operations instead of snapshots, a central sequencer, CRDT ids — exists to guarantee that all copies agree even though edits arrive out of order.
5. Operational Transformation
Operational Transformation (OT) is the older and most battle-tested approach, and it is what classic Google Docs is built on. The idea is to keep editing in terms of operations against integer positions, but to transform each incoming operation so it accounts for the concurrent operations the receiver has already applied. If Bob deleted a character near the start of the document, then Alice's "insert at position 5" needs to become "insert at position 4" before Bob applies it, because his copy is now one character shorter. The transform function adjusts the indices so that both sides converge on the same final text and so that each person's original intention is preserved.
Both sites start from the same document. Alice inserts a character and Bob deletes one, concurrently, so their raw operations reference positions that no longer line up. Each operation is transformed against the concurrent one before being applied, and both sites converge on the identical final text.
The transformation rules are deceptively subtle. There is a small matrix of cases — insert against insert, insert against delete, delete against insert, delete against delete — and each must be defined so that applying the transformed operations in any order yields the same result. Getting these rules provably correct is famously error-prone; several published OT algorithms were later found to have edge cases that failed to converge. The practical consequence is that OT almost always relies on a central server to impose a total order on operations. The server decides the canonical sequence, and clients transform their pending local operations against the server's sequence as it streams back to them. This sidesteps the hardest multi-party transformation cases by reducing every client's job to "reconcile my local edits against one authoritative stream."
# transform op A so it can apply after concurrent op B
function transform(a, b):
a = copy(a) # never mutate the original op
if a.type == INSERT and b.type == INSERT:
if b.pos < a.pos or (b.pos == a.pos and b.site < a.site):
a.pos += 1 # b inserted before a, shift right
if a.type == INSERT and b.type == DELETE:
if b.pos < a.pos:
a.pos -= 1 # b removed a char before a# ... delete-vs-insert and delete-vs-delete cases ...
return a
The OT "diamond". Both sites start at state S. Alice applies a, Bob applies b. Alice then applies b′ = T(b, a) and Bob applies a′ = T(a, b). The transform rules are correct only if both paths reach the same S′.
6. CRDTs
CRDTs have merge/delivery rules designed for convergence. State-based merge is associative, commutative, and idempotent; operation-based designs may require causal delivery or other prerequisites. A grow-only character union cannot implement deletion by itself.
Text sequence CRDT families differ: Logoot/LSEQ use position identifiers; RGA uses predecessor-linked elements; Yjs/Automerge have their own compact structures and causal protocols. They are not all variable-length digit lists sorted the same way. A toy tuple (path,site,counter) also needs a dense allocation scheme between full identifiers: if concurrent IDs share a path, path-only midpoint allocation may not fit between them.
Deleting creates a stable removal/tombstone effect so replaying an old add cannot resurrect text. Safe tombstone collection requires replica/version knowledge or an explicit offline-expiry/resync policy. Site/counter identities must survive restart or use new incarnations. Convergence does not guarantee natural text interleaving or author intention. Use a reviewed library and test its offline/history/access model instead of building a text CRDT from the diagram alone.
7. Central Server vs Peer-to-Peer
Whichever convergence strategy you pick, you still have to decide how operations physically get from one client to another. The two ends of the spectrum are a central sequencing server and a true peer-to-peer mesh, and the choice interacts heavily with OT versus CRDT.
Topology
How ops flow
Fits best with
Central sequencing server
Every client sends ops to one server, which assigns a global order and rebroadcasts. The server is the single source of truth.
OT, which leans on a total order; also simplifies persistence, auth, and access control.
Peer-to-peer
Clients exchange ops directly (or through a relay) with no authoritative ordering; each peer merges independently.
CRDTs, which converge without coordination and tolerate any arrival order.
In practice the overwhelming majority of production systems, including Google Docs, choose a central server even when the underlying data type could go peer-to-peer. A central server is not only the natural home for OT's total ordering — it is also where you do all the boring-but-essential work: authenticating users, enforcing who is allowed to edit, persisting the document durably, and serving the current state to a client that just opened the doc. Pure peer-to-peer is elegant on paper but pushes authentication, persistence, and discovery onto the clients, which is rarely worth it for a document-editing product. The common pattern is therefore "CRDT or OT for correctness, central server for everything operational."
8. Sync Architecture
The concrete architecture is clients talking to a sequencing server over a persistent connection (typically a WebSocket), with the server assigning a monotonically increasing sequence number to every operation and rebroadcasting it to all connected clients. A document store behind the server holds the durable op log and periodic snapshots so that a client opening the document can be brought up to the current state quickly.
A client sends each local operation with the base_seq it was made against. The sequencing server transforms it over any ops the client missed, assigns the next seq, appends it to the document store's op log, ACKs the sender and rebroadcasts the op with its seq to every other client. The store also holds periodic snapshots so a new client can load the current state and replay only the ops since.
Reading the flow:
Clients. Each applies its own edits locally and immediately, then sends them to the server as operations. It also receives the server's ordered stream and reconciles it against any of its own operations still in flight.
Sequencing server. The heart of the design. It receives operations, assigns each a global sequence number (the total order), appends them to the durable log, and rebroadcasts them. For OT it also runs the transform; for a CRDT it can simply relay, since the ids already define the order.
Document store. Persists the operation log so nothing is lost if the server restarts, and stores periodic snapshots so a client does not have to replay millions of operations to open a long-lived document.
# server: transform (OT), assign a global order, persist, then fan out
function on_operation(client, op): # op.base_seq = last server seq the client had seen
for other in store.ops_since(op.base_seq):
op = transform(op, other) # OT: rebase over ops the client missed
op.seq = next_sequence_number() # the total order
store.append(op) # durable before broadcast
client.send(ACK(op.seq)) # sender learns its op's final seq
for peer in connected_clients:
if peer != client:
peer.send(op) # everyone applies same order# CRDT: skip the transform loop; ids already define the order
The ack matters. Without it the sender cannot tell when its op is safely in the log, so it cannot move on to send its next op or know which server sequence number its own edit became. Systems in the Jupiter/Google Docs style keep at most one op (or one batch) in flight per client and send the next only after the ack arrives.
9. Cursors and Presence
A collaborative editor is not only about text — half of what makes it feel alive is seeing where other people are. Each client broadcasts its cursor position and current text selection, usually tagged with the user's name and color, so everyone can render the little labeled carets you see floating in the document.
Crucially, presence data is treated very differently from document operations. Cursor positions are ephemeral: they do not need to be persisted, they do not need exactly-once delivery, and a dropped update just means a cursor lags for a moment. So presence typically rides a separate, best-effort channel rather than the durable op log. When a remote edit shifts text around, each client also has to remap the cursor positions it is displaying, the same way operations are transformed, so that a collaborator's caret stays attached to the right place after an insert or delete upstream of it.
Separating presence from document state is a small decision that pays off: it keeps the reliability-critical op log lean and lets cursors update at a high frequency over a cheap, lossy channel without polluting the durable history.
10. Offline Edits and Reconnection
Reconnect from a durable document version and pending operation IDs. In OT, transform each remote operation against pending local operations and the pending operations against the same pre-transform remote state, following the selected algorithm's control protocol. Sending several rebased pending operations all with one unchanged base sequence is not a complete submission protocol; acknowledge/rebase incrementally or submit a protocol-defined ordered batch.
CRDT reconciliation exchanges compatible state/operations and required causal context; 'any order' is valid only under the implementation's stated delivery/dependency rules. If server history/tombstones were compacted beyond the client's base, require snapshot/resync rather than silently dropping unsent edits. Bind document permissions, version, site incarnation, and dedup identity to every batch. Presence is ephemeral and is not part of the persisted editing history.
11. Tradeoffs
The OT-versus-CRDT decision is the one an interviewer most wants you to reason about out loud. Neither is strictly better; they trade complexity in different places.
Dimension
Operational Transformation
CRDT
Core idea
Transform ops against concurrent ops at apply time.
Design ops so they commute and merge automatically.
Coordination
Usually needs a central server for total ordering.
Converges without a coordinator; peer-to-peer friendly.
Main cost
Transform functions are subtle and easy to get wrong.
Per-character id and tombstone metadata overhead.
Offline tolerance
Workable but needs careful rebasing on reconnect.
Excellent — merges any divergence in any order.
Maturity
Decades of production use (e.g. classic Docs).
Newer, increasingly popular for local-first apps.
The short version for an interview: choose OT when you already have a reliable central server and want a compact on-the-wire format, accepting the burden of getting the transform rules right. Choose a CRDT when you want decentralization, strong offline support, or simpler reasoning about convergence, and you can afford the metadata. Many modern systems even blend the two — using a central server for ordering and persistence while leaning on CRDT-style ids to make merges trivial.
12. Failure Modes
What breaks
Effect
How to handle it
Document server crashes
Editors of its docs disconnect.
Ops are durable before ack; another server takes ownership (lease in a coordination service) and clients reconnect and resend unacked ops.
Client resends an op after a timeout
Same edit applied twice.
Dedup by client_op_id per client.
Two servers both think they own a doc
Two orders, divergent copies.
Ownership lease with a fencing token checked by the op log on append.
Client offline for weeks
Huge merge on reconnect.
Transform or merge against compacted history; if too old, show a conflict copy for the user to review.
Bug in a transform function
Copies silently diverge.
Checksum the document periodically and resync a client from a snapshot when it differs.
Worked example: two insertions at the same position
Both replicas start with AB. Alice inserts X at index 1; Bob inserts Y at index 1; alice sorts before bob.
Read top to bottom. The table below explains the state and guarantee at each step.
Step
State, calculation, and reasoning
1. Apply local edits
Alice sees AXB; Bob sees AYB. Raw remote positions no longer refer to the same state.
2. Transform Bob after Alice
Bob's insertion shifts to index 2, so Alice reaches AXYB.
3. Transform Alice after Bob
Tie-breaking leaves Alice at index 1, so Bob also reaches AXYB.
4. State the limit
This proves one insertion diamond, not a full multi-character OT protocol. Text deletes, pending rebasing, history truncation, and reconnect control require a reviewed algorithm.
Check your understanding: Can every text CRDT be implemented as union and sorting fractional IDs?
No. Sequence CRDTs use different predecessor/position schemes and delete semantics. Dense identifier allocation and causal prerequisites need a complete protocol.
Run a small, testable model
Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:
python3 concepts.py collaboration
python3 concepts.py test
The model tests a deterministic insert-only OT diamond and a simple two-phase set's deletion merge. Neither is a production text editor.
The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.
Text docs, few editors per doc, offline, presence, history.
5–10
Estimate
2 M ops/s in, 6 M out, 2.6 TB/day of ops, so snapshot and compact.
10–15
API
Open doc, WebSocket with op/ack/presence, catch-up by seq.
15–25
High level
Clients ↔ doc server (one owner per doc) → op log + snapshots.
25–38
Deep dive
Why operations not snapshots; OT with a central sequencer vs CRDT ids; the transform diamond; reconnect.
38–45
Wrap up
Presence channel, failover, version history, big documents.
14. Common Mistakes
Mistake 1: sending the whole document
Last write wins and someone's work disappears. Send small operations.
Mistake 2: locking the document or a paragraph
Locks make typing wait on the network and feel broken. Edit optimistically and merge.
Mistake 3: float positions in a CRDT
Concurrent inserts collide and floats run out after about 52 splits. Use (path, site, counter) ids.
Mistake 4: no snapshots
Opening a year-old doc would replay millions of ops.
Mistake 5: putting cursors in the op log
Presence is high-rate and throwaway. Send it on a separate lossy channel.
15. Summary
Collaborative editing is a replicated data structure that must converge no matter how edits interleave. The design is a few reinforcing decisions:
Concern
Mechanism
What is the hard requirement?
Convergence — every copy of the document ends identical despite concurrent, out-of-order edits.
How do we represent a change?
Small position-aware operations (insert/delete) that capture intent, not whole-document snapshots.
How do concurrent edits reconcile (approach A)?
Operational Transformation: transform each op against concurrent ops, usually ordered by a central server.
How do concurrent edits reconcile (approach B)?
CRDTs: unique ordered ids make operations commute and merge with no coordinator.
How do ops travel between clients?
A central sequencing server assigns a total order and rebroadcasts; peer-to-peer is possible with CRDTs.
How do we keep nothing lost?
A document store holding a durable op log plus periodic snapshots for fast joins.
How do we show collaborators?
Ephemeral cursor/selection presence on a separate best-effort channel.
How do we handle offline work?
Edit locally, track last-seen sequence, then catch up and rebase/merge on reconnect.
OT or CRDT?
OT for compactness with a central server; CRDT for decentralization and offline, at a metadata cost.
The recurring theme: local-first, optimistic editing buys responsiveness but forces you to make concurrent edits converge. OT and CRDTs are two answers to that single question, and almost everything else in the design — the sequencing server, the op log, reconnection — exists to make that convergence reliable in the real world.
16. Check Yourself
Alice inserts "x" at position 5 while Bob concurrently deletes position 2. What does Bob apply when Alice's op reaches him?
Answer
Insert "x" at position 4, because Bob's delete removed one character before position 5.
Two users insert into the same gap at the same moment, and both pick the id 1.5. What goes wrong, and how do real CRDTs avoid it?
Answer
The two characters have equal ids, so replicas may order them differently and diverge. Real CRDTs add the site id and a counter to every id, so ids are always unique and ties break the same way everywhere.
Why does the server send an ack back to the sender instead of just broadcasting to the others?
Answer
The sender needs to know its op is durable and which sequence number it received, so it can update its base_seq and send the next op.
A client reconnects with 3 unsent local ops and the server has 10 new ops. Why can't the client just apply the 10 ops as they are?
Answer
Its local copy already includes the 3 local ops, so the remote positions are off. Each remote op must be transformed past the pending ops, and the pending ops past it, before applying and sending.
The op log grows about 2.6 TB a day. What do you do about it?
Answer
Take periodic snapshots, keep only recent ops hot, and move or compact older history. Opening a doc then loads a snapshot plus a short tail of ops.
Why is presence sent on a separate channel from document ops?
Answer
Cursor updates are frequent and ephemeral. Losing one is fine, so they do not belong in the durable, ordered op log.
17. Key Terms
Operation
A small change such as insert or delete at a position.
Operational Transformation (OT)
Rewriting an op so it applies correctly after a concurrent op.
CRDT
Data types with merge rules designed to converge after replicas have received compatible updates. State-based CRDT merges are associative, commutative, and idempotent; operation-based designs impose their own delivery/causality rules. Example and limits above.
Convergence
All replicas end with the same state after seeing the same ops.
Tombstone
A marker left for a deleted character so ids stay valid.
Sequence number
A sequence number records committed order within a stream. A cursor is a consumer's durable position in that stream, so it can resume without skipping missing events. An opaque identity is not automatically such a position. Example and limits above.
Snapshot and compaction
Saving full state so old ops can be dropped or archived.
Presence
Ephemeral info about who is here and where their cursor is.
18. Further Reading
C. A. Ellis and S. J. Gibbs, "Concurrency Control in Groupware Systems," SIGMOD 1989, the original OT paper.
D. Nichols, P. Curtis, M. Dixon and J. Lamping, "High-Latency, Low-Bandwidth Windowing in the Jupiter Collaboration System," UIST 1995, the client-server OT model behind Google Docs.
M. Shapiro, N. Preguiça, C. Baquero and M. Zawirski, "Conflict-Free Replicated Data Types," SSS 2011.
M. Kleppmann, V. Gomes, D. Mulligan and A. Beresford, "Interleaving Anomalies in Collaborative Text Editors," PaPoC 2019.
Yjs documentation (docs.yjs.dev) and the Automerge documentation (automerge.org).