System design interview: design a collaborative editor
A collaborative editor lets every client apply its own edits immediately and sends them as small operations to a session server that owns the document, orders the operations, stores them in a log and broadcasts them over WebSockets. Concurrent edits are reconciled either with operational transformation (OT), where the server rewrites an operation against the ones it has not seen, or with a CRDT, where every character has a unique identity so replicas converge in any order.
By Evin JohnPublished 9 min read
1. Clarify the requirements
Ask what kind of document it is and how people use it. Rich text, spreadsheets and drawings need different operation types, and "must work offline" changes the core algorithm.
- Functional: several people edit the same rich-text document at once; everyone sees others' edits, cursors and selections; edits made offline merge when the device reconnects; per-user undo; version history; owner, editor, commenter and viewer roles.
- Non-functional: every replica ends up with the same content (convergence); an acknowledged edit is never lost; your own typing feels instant; other people's edits appear within a few hundred milliseconds.
- Out of scope unless asked: comments and suggestions, search across documents, export formats.
2. Estimate the scale
These figures are assumptions picked for easy arithmetic, not measurements of any real product.
| Quantity | Assumption | Result |
|---|---|---|
| Concurrent editors | 1 million people with a document open at peak | 1 million WebSocket connections |
| Open documents | 500,000 at peak | 2 editors per open document on average; most documents have one |
| Operations | 1 batched operation a second per connected editor (typists send more, readers none) | about 1 million operations a second in, about 1 million broadcasts out |
| Daily log volume | 300,000 operations a second averaged over the day, 100 bytes each | about 26 billion operations, about 2.6 TB a day before compaction |
| Gateways | 50,000 connections per gateway server | about 20 gateway servers at peak |
| Session servers | 20,000 operations a second each; 100 KB in memory per open document | about 50 servers, holding about 50 GB of documents in total |
The conclusions: storage is not the problem; ordering many small writes and fanning them out quickly is. One document's traffic is tiny, so a single server per document is enough, which makes ordering easy.
3. Define the API
GET /api/v1/docs/{docId}
returns: 200 { "version": 1203, "snapshot": { ... }, "role": "editor" }
WS /api/v1/docs/{docId}/live?since=1203
client -> server { "type": "op", "clientId": "c42", "seq": 17, "baseVersion": 1203,
"ops": [{ "retain": 120 }, { "insert": "hello" }] }
server -> client { "type": "ack", "seq": 17, "version": 1204 }
server -> others { "type": "op", "version": 1204, "clientId": "c42", "ops": [ ... ] }
both ways { "type": "presence", "userId": "u9", "cursor": { "anchor": 125, "head": 125 } }
GET /api/v1/docs/{docId}/history?from=1000&to=1204
POST /api/v1/docs/{docId}/permissions { "email": "ana@example.com", "role": "commenter" }Each client sends at most one unacknowledged batch at a time and buffers later edits until the acknowledgement arrives. That keeps the transformation logic on both sides small. clientId plus seq lets the server drop a batch it has already applied when a client resends after a reconnect.
4. Sketch the data model
| Table | Key fields | Why it is there |
|---|---|---|
documents | doc_id, owner_id, title, latest_snapshot_version | The document's identity and where loading starts |
doc_ops | (doc_id, version) primary key, client_id, client_seq, user_id, ops, created_at | The append-only log; unique (doc_id, client_id, client_seq) makes resends harmless |
snapshots | (doc_id, version), pointer to the serialized document | Full state every N operations, so loading never replays the whole history |
permissions | doc_id, principal_id, role | Checked when a session opens and on every write |
| Routing (coordination service) | doc_id to session server, epoch | Which server owns the document right now |
Presence (who is here, where their cursor is) lives only in the session server's memory. Persisting every cursor move would multiply writes for data nobody needs tomorrow.
5. The core decision: OT or a CRDT
Both approaches make concurrent edits converge. They differ in how they describe a position in the text and in whether they need a central ordering point.
Operational transformation. Operations use integer positions relative to a specific version. When an operation arrives based on an older version, the server transforms it against each operation applied since, so it still means what the author intended. Take the text cat. Ana inserts s at position 3; at the same moment Ben deletes position 0. The server applies Ana's first, giving cats at version 1. Ben's delete was based on version 0; Ana's insert came after position 0, so the delete stays at 0, and the result is ats. Ben's client, which already shows at, receives Ana's insert transformed against his delete: position 3 becomes 2, giving ats. Both converge.
With a central server that fixes a single order, the transform function only has to handle two concurrent operations correctly. Without a central server, OT needs extra properties that are hard to get right, and several published peer-to-peer OT algorithms were later shown to diverge in some cases. That is why practical OT systems put a server in the middle.
CRDTs (conflict-free replicated data types). Each inserted character gets a permanent unique id, such as (replica id, counter), and an insert says "after the character with id X" rather than "at position 3". Deleted characters usually stay as tombstones so later operations can still refer to them. The merge rules are commutative and idempotent, so any two replicas that have received the same set of operations hold the same text, whatever the order. Operation-based CRDTs rely on the sync layer to deliver each operation in causal order (and to ignore repeats); state-based CRDTs merge whole states and tolerate any delivery order.
| OT with a central server | CRDT | |
|---|---|---|
| Needs a central order | Yes | No; a server is still useful for storage, access control and relay |
| Metadata cost | Small: integer positions | An id per character plus tombstones; modern libraries compress runs of characters |
| Long offline edits | Works, but a long offline branch must be transformed against a long server history | Merges naturally; this is the main reason to choose one |
| Where the complexity sits | Transform functions for every pair of operation types | Data structure design, memory use, garbage-collecting tombstones |
| Open-source examples | ShareDB | Yjs, Automerge |
A reasonable default for an online-first product is OT with one session server per document, because the server is already the source of truth. If the interviewer stresses offline-first or peer-to-peer use, switch to a CRDT; the rest of the architecture barely changes, except that the server merges and relays instead of transforming. Neither approach understands meaning: if two people retype the same sentence differently, both edits survive and a human tidies up.
6. The architecture
- Clients
- Editor with local document copy
- Pending-operation queue in local storage
- Edge
- Load balancer
- WebSocket gateways (auth, connection state)
- Routing
- Document-to-server routing with leases and epochs
- Session tier
- Session server per document (order, transform, presence)
- Permission check
- Storage
- Operation log, keyed by document and version
- Snapshot store (object storage)
- Document and permission metadata
- Async
- Snapshot worker
- Version history service
- Search and export indexers
- Apply locallyThe client applies the edit to its own copy at once, then sends it with its base version.
- OrderThe session server transforms the operation against anything newer than its base version and assigns the next version number.
- PersistIt appends the operation to the log with a conditional write on (document, version) and only then acknowledges the author.
- Fan outIt broadcasts the operation to the other clients, which transform it against their own unacknowledged edits and apply it.
7. Scale it and handle the hard features
- Shard documents to session servers. Use consistent hashing on
doc_idor an explicit routing table with leases. One writer per document keeps ordering trivial; capacity grows by adding servers. See sharding strategies. - Snapshots. Write a snapshot every 1,000 operations or 5 minutes (assumed tuning). Load is snapshot plus tail, and old operations can be compacted into named versions for history.
- Presence. Send cursor updates at most every 50 to 100 ms and transform cursor positions like operations (with a CRDT, anchor them to character ids). Drop a user's presence when their connection closes or a short timeout passes.
- Offline edits. The client keeps unsent operations in local storage with their base version. On reconnect it sends them; the server transforms them against everything since (OT) or merges them (CRDT). If the needed history has been compacted away, fall back to a three-way merge against a snapshot, and never drop the user's work silently.
- Undo. Undo is per user and never rewrites history. The client takes the inverse of the user's last operation, transforms it against everything applied since, and sends it as a new operation. With a CRDT, undo is a new operation that deletes what you inserted or restores what you deleted.
- Permissions. Check the role when a session opens and on every write. When a role is downgraded, the session server pushes the change and starts rejecting operations from that user at once.
- Very busy documents. For thousands of viewers, broadcast through a separate fan-out tier, cap simultaneous editors and batch operations.
- Regions. One session server per document means collaborators on another continent see edits a cross-region round trip later, often 150 ms or more. Place each document's server near most of its editors and move it when that changes. Consistency comes first here; see consistency and CAP.
8. What breaks, and what you do about it
| Failure | Effect | Mitigation |
|---|---|---|
| Session server crashes | Open documents on it pause; unacknowledged operations are lost on the server | Routing moves the document; the new server loads snapshot plus log; clients resend unacknowledged operations, deduplicated by client id and sequence |
| Two servers own one document | Two different operations claim the same version | Ownership epochs; the conditional append on (document, version) lets only one succeed |
| Gateway dies | Thousands of clients reconnect at once | Reconnect with exponential backoff and jitter; clients rejoin with their last acknowledged version |
| Slow client | Its outbound buffer grows on the server | Bound the buffer; when it overflows, close the socket and let the client reload from a snapshot |
| Bad snapshot | Every load starts from a wrong state | Keep earlier snapshots and the log; periodically rebuild a snapshot by replay and compare checksums |
9. Trade-offs to discuss
- OT or CRDT. OT is lighter on metadata and well proven with a central server; CRDTs remove the ordering point and handle offline merges, at the price of per-character metadata.
- Persist before broadcast. Persisting first adds a log write (a few milliseconds) to the collaborator's latency but means nobody sees an edit that could still be lost. Broadcasting first is faster and occasionally shows text that later disappears.
- Batch size. Sending every keystroke gives the smoothest remote view but the most messages; batching for 50 to 100 ms cuts message volume sharply at a delay people barely notice.
- Snapshot frequency. Frequent snapshots make loads and recovery fast but cost storage and write bandwidth.
- Rich text. Formatting spans (bold that two people extend at once) need their own merge rules in either model; name this as the next hard problem.
10. How to talk through it in the interview
Fictional example
- Interviewer
- Why does every editor of a document go to the same server?
- Candidate
- Because it gives every operation a single version number. With one order, the server only transforms an incoming edit against the edits it has applied since the client's base version, and each client does the mirror image for its own pending edit. Without that order I would need a CRDT.
- Interviewer
- And when that server dies?
- Candidate
- Nothing acknowledged is lost, because I append to the log before I acknowledge. The routing service gives the document to another server with a higher epoch, it loads the latest snapshot and the log tail, and clients resend whatever was not acknowledged.
- Walk through one tiny concurrent edit, like the
catexample, on the whiteboard. It proves you understand the merge model. - Say clearly which guarantee each piece gives: convergence from the merge model, durability from the log, ordering from the single session server.
- Keep presence separate and cheap, and say why.
Practicing this design with Natively
Run the walk-through as a mock interview: ask a friend to play the interviewer on a call, or read the prompt aloud yourself. Natively transcribes the conversation as it happens and can suggest talking points for each question as it comes in; comparing them with what you said is a quick way to spot gaps such as offline reconciliation or undo. If you sketch the OT example or the architecture, you can capture a screenshot of it and have Natively explain it. After the session, the transcript and notes show where you ran long and what you skipped.
Questions people ask
What is the difference between OT and CRDTs?
Operational transformation uses integer positions and rewrites an incoming edit against concurrent edits the sender had not seen, which is straightforward when a central server fixes one order. A CRDT gives every character a unique id and uses merge rules that converge in any delivery order, so it needs no central order and handles offline edits well, at the cost of extra metadata per character.
Why route all editors of a document to one server?
A single session server gives every operation one version number, which makes ordering and OT transformation simple. One document's traffic is small, so one server can handle it, and capacity grows by spreading documents across many servers.
How does undo work in a collaborative editor?
Undo is per user and is sent as a new operation, never as a rewrite of history. The client takes the inverse of the user's last edit, transforms it against everything applied since, and submits it. In a CRDT, undo is likewise a new operation that removes what you inserted or restores what you deleted.
Where to go next
Practice with the same assistant you would use on a call: it listens, transcribes and suggests answers from your own resume and the job description.
Facts on this page were last checked on . Product details come from the Natively app itself; if something here is out of date, tell us at hello@heynatively.com.
