Mid to Senior Engineer

System Design Interview Prep

A structured path from the interview framework through core concepts, key technologies and patterns to eighteen full problem breakdowns, each with diagrams and weak, solid and excellent answers to every deep dive.

Chapter 32 of 36Problem breakdowns · Design Collaborative Document Editing

Design Collaborative Document Editing

Collaborative editors let several people change the same document at the same time and see each other's changes appear within a moment. The interesting problem is convergence: if two people type in the same sentence at once, every copy of the document must end up identical, and nobody's edit may be lost. It looks like a chat problem and it is much harder, because edits to shared text do not commute.

This chapter explains the two standard answers, operational transformation and conflict-free replicated data types, then builds the system around them. It follows the usual shape: understand the problem, set up the interface, build the high-level design, then go deep on the questions interviewers use to separate levels.

1. Understanding the problem

Multiple users open the same document. Each sees the text, sees where others are editing, and sees changes as they happen. Edits made offline are merged when the user reconnects.

Functional requirements

Core:

  1. Several users edit one document simultaneously, and everyone sees the same result.
  2. Changes appear to others within a fraction of a second.
  3. The document is saved durably, and users can reopen it later, with history.

Confirm in or out: rich text and formatting, comments, version history and restore, offline editing, permissions and sharing, and very large documents. A sensible opening: "I will design real-time plain-text co-editing with durable storage and history, then discuss rich text, offline editing and permissions."

Non-functional requirements

  • Convergence. All replicas of the document end in the same state, always.
  • Intention preservation. An edit should do what its author meant, even after other edits arrive.
  • Low latency. The user's own typing must feel instant, so edits apply locally first.
  • Durability. No acknowledged edit is lost.
  • Availability and graceful behaviour on poor networks.
  • Scale. Assume millions of documents, but a typical document has a handful of simultaneous editors.

Estimation

QuantityCalculationResult
Editsan active typist produces a few operations per second3 per second per editor
Concurrent active editorssay 10 million30 million operations per second
Per documenttypically 1 to 5 active editors, rarely hundredstiny per document
Operation sizetens of bytessmall

What the numbers say. The aggregate rate is large, and it is spread over millions of independent documents, each with a very small rate. That suggests partitioning by document: each document is handled by one server at a time, which can order its operations without any distributed coordination.

2. The set up

Core entities

  • Document: identifier, owner, permissions.
  • Operation: an edit such as insert or delete, with a position, the author and the document version it was based on.
  • Snapshot: the full document content at a version.
  • Session: an editor's connection to a document.

Protocol

Editing runs over a persistent connection (WebSocket):

client -> server   { "doc": "d1", "op": { "type": "insert", "pos": 3, "text": "s" }, "base_version": 41 }
server -> clients  { "doc": "d1", "op": { ... transformed ... }, "version": 42, "author": "u7" }

Everything else, such as creating, listing and sharing documents, is ordinary request and response over HTTP.

3. The core problem: concurrent edits

Suppose the document is cat. User A inserts s at the end, giving cats. At the same moment, user B inserts h at the start, giving hcat. Each applies their own edit locally, then sends it to the other.

If A applies B's operation "insert h at position 0" to cats, the result is hcats, which is correct. If B applies A's operation "insert s at position 3" to hcat, the characters are h, c, a, t at positions 0 to 3, so inserting at position 3 gives hcast, which is wrong. The position in A's operation referred to the text before B's edit. B's insertion shifted everything right by one, so A's position is now stale, and the two users end with different documents.

This is the heart of the problem. Operations are written against a version of the document, and when concurrent operations arrive, they must be adjusted or the data they act on must be designed so that adjustment is unnecessary. The two main approaches differ in exactly that.

4. Operational transformation

Operational transformation (OT) adjusts operations. When an operation arrives that was made against an older version, the server transforms it against every operation that happened since, so its position is correct for the current text.

<!--fig:ot-->
Start: "cat"both users see the same textUser A: insert "s" at 3locally: "cats"User B: insert "h" at 0locally: "hcat"Server transformsA arrives first: apply, "cats".B was made against "cat", soinsert at 0 is unaffected by A.Send A to B, and B to A, withpositions adjusted if needed.Both converge on "hcats".If B had inserted at 3 too, the transform shifts one insertion by one place, so both are kept in a fixed order. Figure 1. Operational transformation: concurrent edits are transformed so every copy converges.

In the example, B's insert at position 0 is transformed against A's insert at position 3: since A's insertion comes after position 0, B's operation is unaffected. A's operation, transformed against B's, shifts from position 3 to position 4, because B inserted a character before it. Both documents end as hcats.

How it works in a client and server system:

  1. The server keeps the authoritative sequence of operations, each with a version number.
  2. A client sends an operation along with the version it was based on.
  3. The server transforms the operation against any operations committed after that version, applies it, assigns it the next version and broadcasts it.
  4. Each client, on receiving others' operations, transforms them against its own unacknowledged local operations before applying them, so its local text stays consistent with the server's.

Properties. OT works well with a central server that defines the order, which is simple and fits most products. It needs a correct transformation function for every pair of operation types, which is notoriously hard to get right. Rich text, with formatting, lists and tables, multiplies the number of pairs.

5. Conflict-free replicated data types

A CRDT takes the opposite approach: design the data structure so that any order of applying operations yields the same result, so no transformation is needed and no central ordering is required.

For text, a common idea: give every character a unique, permanent identifier instead of a numeric position, and define an ordering on identifiers. An insert says "place this character between character X and character Y," naming them by identifier. Because identifiers do not shift when other characters are inserted, the instruction means the same thing on every replica, whatever the order of arrival.

For example, instead of "insert s at position 3", the operation is "insert s after the character with identifier c1:3". A replica that has received that character can apply it immediately and get the same result as every other replica. A delete marks a character with a tombstone rather than removing it, so later operations that refer to it still make sense.

Properties.

  • Replicas converge without a central server, which suits peer-to-peer and offline editing.
  • No transformation functions are needed, because the structure itself guarantees convergence.
  • Cost: identifiers and tombstones add metadata overhead, so memory and storage grow beyond the text size, and clever designs compress or garbage collect them. Interleaving of concurrent inserts at the same place needs careful definition of ordering so that text from two users does not mix character by character.

Choosing between them

AspectOTCRDT
Needs a central ordering serverNormally yesNo
Offline and peer-to-peer editingHarderNatural
ComplexityTransformation functionsData structure design
Metadata overheadLowHigher
Maturity in large productsLong track recordIncreasingly common

A balanced interview answer: "I would use a central server per document, which makes ordering simple, with either OT or a CRDT. OT has low overhead and a long history. A CRDT makes offline editing and convergence easier to reason about, at the cost of metadata. For a first version I would choose whichever my team can implement correctly, and use a well-tested library rather than writing my own."

6. High-level design

Documents are independent, so each document is handled by one session server at a time. That server holds the document's current state in memory, orders (or merges) incoming operations, appends them to a durable log and broadcasts them.

<!--fig:hld-->
ops append Editor A Editor B Editor C offline, later WebSocketgateway Session server owns this document Operation log append-only, durable Snapshots periodic Document registry doc to server Figure 2. Editing sessions: a document is owned by one session server that orders operations; operations are logged durably and snapshotted.
  • WebSocket gateways hold client connections.
  • A document registry (a small, fast, strongly consistent store) records which session server owns each active document, so all of its editors are routed to the same place.
  • The session server applies operations, assigns versions, broadcasts to the connected editors, and appends to the operation log.
  • Periodic snapshots capture the document so that loading does not need to replay the whole history.
  • Offline editors reconnect and send their buffered operations, which are transformed or merged.

7. Potential deep dives

Deep dive 1: How do you make typing feel instant?

The challenge. Waiting for the server before showing a typed character would make every keystroke feel laggy.

Weak: send the edit and wait for the server to echo it back. Each keystroke takes a round trip, which is unusable on a slow connection.

Solid: apply edits locally at once, and reconcile. The client shows its own edit immediately and sends it in the background. When the server acknowledges it, the client moves on. Edits from others are transformed or merged against local pending edits.

Excellent: an optimistic client with a clear model of pending state. The client keeps three things: the last server-confirmed state, a queue of sent-but-unacknowledged operations, and a buffer of operations not yet sent (batched and compressed to limit traffic). Incoming remote operations are transformed against both local queues. It handles reconnects by resending unacknowledged operations, which the server deduplicates by operation identifier, and it rebases on the server's current version. Latency stays low and the result still converges.

Deep dive 2: How do you scale across millions of documents?

The challenge. Many documents are active at once, and each needs a single point of ordering.

Weak: any server handles any operation, with a shared database for ordering. Every operation contends on shared state, and ordering requires a distributed lock or transaction.

Solid: partition by document, with one owner per document. Route all editors of a document to one session server, which orders operations in memory and appends them to a log. Servers scale out by owning different documents.

Excellent: ownership with leases, fast failover and hot-document handling. Record ownership in the registry with a lease, so that if a session server dies, another can take over the document after the lease expires, rebuilding state from the latest snapshot plus the log. Fence the old owner to avoid two servers ordering the same document. A document with hundreds of editors, such as a public one, is a hot spot: keep the single ordering point but offload fan-out to a layer of broadcast servers, and consider limiting simultaneous editors or switching extra viewers to read-only mode.

Deep dive 3: How do you store and restore history?

The challenge. Users want the latest text fast, plus version history and the ability to restore.

Weak: store only the latest text. No history, and a bad edit cannot be undone.

Solid: store the operation log, and compute state by replay. The log is the history, and the state at any version is the result of replaying operations up to it.

Excellent: the log plus snapshots and compaction. Replaying a long history on every open is slow, so write a snapshot every so often (every N operations or minutes) and load the latest snapshot plus the later operations. Group operations into named versions for the history view, such as one entry per editing session, rather than listing every keystroke. Compact old log segments into snapshots, and keep the full log for a retention period or for audit. Restoring a version creates a new operation that sets the content, so history is never rewritten.

Deep dive 4: Offline editing and reconnection

The challenge. A user edits on a train with no signal, and meanwhile others keep editing.

Weak: block editing offline. Users lose work and trust.

Solid: queue operations locally and replay on reconnect. Store pending operations durably on the device, and send them when the connection returns, transforming them against what happened meanwhile.

Excellent: merge with a data type that supports it. This is where CRDTs shine: replicas edited independently converge when exchanged. With OT, the server must transform a long offline sequence against a long server history, which is costly and can produce odd results for large divergences. Show the user what changed on reconnect, and keep a way to recover the pre-merge version. Bound the divergence you accept, and for extreme cases prompt the user to review.

Deep dive 5: Presence and cursors

Showing who is in the document and where their cursor is makes collaboration feel alive, and generates a lot of traffic.

  • Send cursor and selection updates at a limited rate (a few per second), coalescing to the latest.
  • Treat presence as ephemeral: keep it in memory on the session server, do not persist it, and expire it with heartbeats.
  • Express cursor positions in terms the transformation understands, so that a cursor moves correctly when others insert text before it.

Deep dive 6: Permissions and security

Check authorisation on every connection and operation: viewer, commenter, editor, owner. A viewer's connection must reject edit operations at the server. Authenticate the WebSocket at connection time with a short-lived token, and revoke access promptly when sharing changes, which means disconnecting existing sessions. Treat the document content as sensitive, encrypt it in transit and at rest, and keep an audit log of access. Validate the operations themselves, so that a malicious client cannot send an operation that corrupts state (a position outside the document, an enormous insert).

Deep dive 7: Rich text and structure

Plain text is a sequence of characters. Rich text adds formatting, paragraphs, lists, tables and embedded objects. Model the document as a tree or a sequence with attributes and define operations on that model: insert text, delete, set an attribute on a range, insert a node. The more operation types, the more cases the transformation (or the CRDT design) must handle, which is why most teams build on an existing, well-tested editing library and framework. Say that in an interview rather than claiming you would design the rich-text algebra from scratch.

8. What is expected at each level

Mid-level. You recognise that concurrent edits conflict and that a naive last-write-wins loses data. You propose sending operations through a server over WebSockets and storing the document durably.

Senior. You explain operational transformation or a CRDT with a concrete example, apply edits optimistically on the client, partition documents to one owner, and use an operation log with snapshots.

Staff. You compare OT and CRDTs for the product's needs, discuss offline and peer-to-peer implications, ownership failover with fencing, hot documents, rich text complexity, permissions revocation, and how you would test convergence, for example by randomised concurrent-edit simulation.

9. Interview questions and model answers

Q: Why can't you just apply edits in the order they arrive? Each edit's position refers to the document as the author saw it. If another edit arrives first and shifts the text, the position is stale and the edit lands in the wrong place, so replicas diverge. Operations must be transformed, or designed so order does not matter.

Q: OT or CRDT? With a central server per document, OT is simple in metadata and proven, but needs correct transformation functions for every operation pair. CRDTs converge without a central order and suit offline and peer-to-peer use, at the cost of identifier and tombstone overhead. I would use a tested library either way.

Q: How do you make typing feel instant? Apply the edit locally immediately, send it in the background, and reconcile with the server's order. The client keeps a queue of unacknowledged operations and transforms incoming remote operations against it.

Q: How do you scale to millions of documents? Partition by document: one session server owns each active document, orders its operations, logs them durably and broadcasts. Ownership is leased in a registry, with failover from the snapshot and log.

Q: How do you store history? An append-only operation log plus periodic snapshots, so opening loads the latest snapshot and replays recent operations. Versions are grouped for display, and restore is a new operation.

Q: How do you handle a user who is offline for a day? Queue their operations locally, and merge on reconnect, which is natural with a CRDT and costly with OT for large divergence. Show them the merged result and keep a way to recover the earlier version.

10. Common mistakes

  • Last-write-wins on the whole document, which destroys concurrent edits.
  • Using numeric positions without transforming them against concurrent operations.
  • Waiting for the server on every keystroke.
  • A shared database ordering all documents instead of one owner per document.
  • Replaying the full history on every document open.
  • Persisting every cursor movement.
  • Checking permissions only when the document is opened.
Header Logo