Design a Collaborative Document Editor
- Scope a live shared document where crossing edits must still converge on every screen
- Grow the design from convergence: an operation model, one ordering point, an op log
- Go deep on operational transformation, per-user undo, catch-up, presence and snapshots
Understanding the problem
Design a collaborative document editor. Several people open the same document and edit it at once, and each person sees everyone else's changes live, character by character.
On its face it's a chat app for text. The difficulty shows up the moment two people type into the exact same spot at once, neither saw the other, and both edits are already on the wire. Whose change wins, and how does every screen end up showing the same thing? Answer that and you have a real editor. Dodge it and you have a toy. That gap is exactly why Adobe reaches for this prompt.
Concurrent editing is hard for one specific reason. A position in text only means something against the exact characters it was written against. "Delete the character at index 1" was aimed at one string; once someone else inserts a character ahead of index 1, that same instruction now deletes the wrong character. Most designs let you dodge strict consistency by relaxing it and nobody notices. This one cannot, because here the copies converging is the product. If two screens disagree about the text after a tangle of edits, the editor is broken, and there is no "eventually nobody cares" escape hatch when the disagreement is the document itself.
Fix the latency budget before drawing anything, because it sets the architecture. A keystroke should land on a collaborator's screen in well under a second, ideally inside about 100 milliseconds, or the shared cursor feels broken and people start typing over each other. That rules out the request-and-poll pattern most web apps run on. Edits have to be pushed, both directions, over a connection that stays open. Settle that early. Every later choice hangs off it.
Functional requirements
Propose a tight scope and leave room for the interviewer to steer. The core is small. People open a shared document, edit its text together in real time, and see each other's changes and cursors as they happen. The document persists, so anyone reopening it gets the latest state, and any editor can undo and redo their own work. Everything else can be named and set aside.
In scope
- openDocument: load the current text and join the live session
- Concurrent editing: several people insert and delete in the same document at once, with edits propagating to every collaborator live
- Convergence: after any tangle of concurrent edits, every copy ends identical and no keystroke is silently dropped
- Per-user undo and redo: each editor walks back their own edits without disturbing anyone else's
- Presence: who is in the document and where their cursors are
- Durable storage so the document survives, reloads, and can rebuild after a crash
Below the line (call it out, then skip)
- Rich formatting, tables, images, and embeds (the same machinery extends to them, but plain text shows the hard part)
- Comments, suggestions, and the revision-history UI
- Access control, sharing, and permissions
- Long offline editing then reconciling days later (a genuinely harder design we will gesture at)
- Search across documents
Non-functional requirements
These carry the design, so reason through each with a target. Convergence is the headline and it has a precise meaning here. After any interleaving of concurrent edits, every collaborator's copy ends byte-for-byte identical, and no acknowledged keystroke is ever lost. Low latency backs the feel: that sub-100-millisecond propagation, because the editor only seems collaborative when the lag is invisible. Durability covers the text itself: once an edit is acknowledged, it survives a server crash, because losing an acknowledged edit corrupts a document someone is actively trusting.
Flag one relaxation now and spend it later. Presence, the live cursors and the "3 people editing" banner, is allowed to be lossy and a little stale. If a cursor blip arrives a beat late or never, nobody's document is wrong. Only the text has to converge, and the decoration around it can slip. A durable ordered edit stream and an ephemeral lossy presence stream are two transport problems with opposite guarantees. Name the split early, and the expensive guarantees cover only the text.
Put rough numbers on the load. Treat this at Adobe scale: hundreds of millions of documents in storage, but a given live document has only a handful of people in it. Most have one to five editors, and a busy shared review might run up to about 50. An active typist commits maybe 5 edits a second, counting keystrokes and the occasional paste. A hot 50-person document then sends about 250 edits a second to one ordering point. Each fans out to the other 49, so roughly 12,000 messages a second leave a single server for that one document. That is comfortable for one box. The aggregate across the fleet is enormous, but it spreads cleanly, because edits for one document only ever touch that document's session. The pressure is never the total. It is the single hot document, and that is the number the design has to survive.
- Convergence: All copies end identical. After any concurrent interleaving every screen agrees on the text; no acknowledged edit is dropped
- Latency: Edit to screen <~100ms. Sub-second propagation rules out polling and forces a persistent pushed connection
- Editors per doc: ~1 to 5, up to ~50 hot. A 50-person doc is ~250 edits/sec in, ~12k messages/sec out of one ordering point
- Documents: Hundreds of millions. Tiny metadata per doc, large append-only op logs; load spreads because each doc is independent
The set-up
Core entities
Name the handful of objects the system moves around before drawing anything. Four carry the design:
Document
The logical document: an id, owner, title, and a current version number that increments with every committed edit. The version is the lever the rest of the design pulls on, because it pins exactly which text an edit was written against and exactly how far behind a reconnecting client is.
Operation
One atomic edit: insert a character at a position, or delete a range. Each operation is tagged with the document version it was created against and the user who made it. This is the unit that flows over the wire and lands in the log, ordered along the way. Keeping operations tiny, just a type and a position plus the one character, is what keeps conflict resolution tractable.
Session
One live connection from one editor: who they are, their cursor position, the version their client has caught up to, and the edits they have made but the server has not yet acknowledged. The session is also where a user's personal undo and redo stacks live, which is what makes undo per-user rather than global.
Snapshot
The full materialized text of a document as of some version, saved periodically so a fresh client can be seeded without replaying the entire operation history. A snapshot is a write-once blob tagged with the version it covers.
One split runs through the rest of the design, so make it explicit now. A document is, at bottom, an ordered list of operations. Insert "h" at 0, insert "i" at 1, and so on. Replay them in order and you get the text. Storing that as an append-only Op Log gives the system durability, a full revision history and the raw material for undo, since nothing is ever overwritten. The materialized text in the Document Store is then just a cached view of the log: the latest snapshot plus the few operations logged since. A relational store keyed by document id holds the tiny metadata, the Document row and its current version. The hard engineering is keeping the ordered log of edits and the snapshot of the result consistent under concurrent and out-of-order edits. So the deep dives below are mostly about convergence.
API
The hot path is not request and response. The client opens one long-lived connection and sends and receives a stream of operations over it, and the live edit channel is a server-push: the server sends each committed edit down the socket without the client asking. Only the bookkeeping, opening the document and loading a snapshot, is plain HTTP. The interesting endpoint is onCommittedOp, the broadcast push, because it is what makes the editor feel live and it is the one the whole convergence story flows through.
Two details carry the contract. The applyOperation call always rides its baseVersion, because an edit is only meaningful against the text it was written on, and the server needs to know how far the document moved since. The onCommittedOp push is shaped the opposite way from a normal response. It arrives later and unprompted, and it can land at any client in a different order relative to that client's own pending edits. Reconciling it against those pending edits on the receiving side is what every screen does to converge. Everything hard about this design lives in those two endpoints.
High-level design
Build the simplest thing that works, then walk an edit through it. A keystroke goes up an open connection, one authority orders it against everything committed since, appends it to a durable log, and pushes it back down to everyone else. Ordering and broadcast are deliberately one place per document, because convergence needs a single source of truth for the order of edits, and you cannot get that from many servers each deciding on their own. In the diagram, color marks each role. Blue is the Clients. Violet covers the services: the WebSocket Gateway that holds the open sockets, the Document Service that orders and transforms edits, and the Presence Service that fans out cursors. Amber is the Op Log that durably records committed edits. Green is the Document Store holding snapshots and metadata.
The pieces connect like this. A Clients member opens the document with openDocument, which returns the latest snapshot from the Document Store plus the version it reflects, then upgrades to a persistent WebSocket on the WebSocket Gateway. The gateway routes that socket to the one Document Service instance that owns this document. When the user types, the client applies the change locally and sends the operation up, tagged with its base version. The Document Service transforms it against anything committed since, commits it, appends it to the Op Log, and pushes it back through the WebSocket Gateway to every other editor. Periodically the log rolls into a fresh snapshot in the Document Store, while cursors take the side path through the Presence Service and never touch the log.
1) Connect and load is durable the instant it returns. A Clients member opens the document, the Document Service hands back the latest snapshot and version, and the socket goes up. One service owns a given document at a time, and that single ownership is what lets it impose one order on edits without coordinating with anyone. The client now holds the text and a version number and is caught up. What this deliberately keeps off the path is the question of what happens when that owning service dies mid-session, or a client briefly drops. The sync deep dive picks that recovery up.
2) Editing applies locally first, then orders centrally. When the editor types, the client applies the change to its own copy immediately, so the cursor never stutters, then sends the operation up tagged with the version it was written against. The Document Service is the ordering point. It transforms the incoming operation against any operations committed since that version, applies it, bumps the document version, and appends the committed operation to the Op Log. Local-first application keeps typing responsive while the server reconciles afterward. The transform step under genuinely concurrent edits is the entire reason this design is hard, and the first deep dive takes it apart.
3) Broadcast is a small, bounded fan-out. Once an operation is committed, the Document Service pushes it back through the WebSocket Gateway down every other open socket with its new version number. Each receiving client transforms the incoming operation against its own edits that the server has not yet acknowledged, then applies it, so every screen converges on the same text. The fan-out is bounded by the editors in this one document, around 50 at most rather than millions, so a single server handles a document's whole broadcast comfortably. Keeping a client correct after it falls behind this stream gets its own deep dive on ordering and sync.
4) Presence rides a cheaper, separate path. Cursor positions and the "who is here" list go through the Presence Service, which fans them out to the other sessions and logs nothing. If a cursor update is dropped, the next one corrects it within a fraction of a second, and no document state was ever at risk. Keeping presence off the durable edit path is what lets the edit path stay strict, since the expensive guarantees only have to cover the text. A later deep dive separates the two paths properly.
5) Persistence and snapshots keep reopening fast. The Op Log is the source of truth: every committed operation is appended, so the document at any version is just the operations up to that version replayed. Replaying from the beginning on every open would be slow, so the log periodically rolls into a snapshot in the Document Store tagged with the version it covers. Opening a document then loads the latest snapshot and replays only the operations logged since. The log also carries undo and history at no extra cost, because the whole past is still there. Sizing the snapshot cadence and making undo work against a shared log is what the persistence deep dive sorts out.
Technology choices
Every box above is a category. In the room, name a real technology for each and be ready to defend it. The interviewer's favorite follow-up is the failure question: what happens to people's edits when the server holding a live document dies, or a socket drops for ten seconds? For each pick below, the reason that kind of tool fits, and how the design behaves when it breaks.
- The WebSocket Gateway is a fleet of stateful WebSocket servers built on something like Netty or a Go service, holding the open sockets in memory. The editor needs the server to push edits the instant they commit, and request-and-poll cannot do that at sub-100-millisecond latency. Each document is owned by exactly one Document Service instance. A router maps each document id to its current owner with consistent hashing or a coordination service such as ZooKeeper, so every editor of one document lands on the same box. When a gateway node dies, its clients simply reconnect, the router points them at the live owner, and each replays from its last-seen version. Stateless edit data on a stateful connection: the connection can be rebuilt, the edits cannot be lost because they live in the log.
- The Document Service is an in-memory, single-owner-per-document service, and the in-memory part is the point. It holds the live document and orders edits without a database round-trip per keystroke, which is the only way to hit the latency budget. Single ownership is what gives one canonical order, so convergence is a matter of transforming against a known committed sequence rather than refereeing many servers. The cost shows up on failure. When an owning instance dies, in-memory edits it never logged could vanish. So it appends to the Op Log before acknowledging an edit. On failure the router reassigns the document, the new owner rebuilds from the last snapshot plus the log tail, and clients reconnect and re-sync. That recovery is the sync deep dive.
- The Op Log is an append-only log per document, which Kafka models cleanly, one partition's worth of ordered durable records per document, or a purpose-built log store does. A log fits because edits need order, durability and replay at once. Each record replicates across brokers, so a dead broker loses nothing. A reconnecting client just reads forward from its last-seen offset. Appends are cheap, which matters at 250 edits a second on a hot document. A relational row-per-character would buckle under that write rate and gives nothing the log does not.
- The Document Store is two stores wearing one box on the diagram. Snapshots are write-once blobs of materialized text in object storage like S3, replicated across zones. A snapshot worker garbage-collects log records older than the newest snapshot once that history is past the horizon anyone replays from. The small per-document metadata, title and owner and current version, lives in Postgres sharded by document id, where every lookup hits one document and read replicas can serve a slightly stale version pointer harmlessly. Splitting big write-once snapshots from tiny mutable metadata puts each on the store it suits.
- The Presence Service is Redis pub/sub, because presence is ephemeral and lossy by design. Cursors live in memory keyed by document, fan out to the document's subscribers, and expire on a short timeout when a client goes quiet. If the relay drops a message or even restarts, the next cursor update repairs the view within a fraction of a second, so no durability is needed and none is paid for. Running cursors through the durable edit pipeline instead would make them needlessly expensive and, worse, couple cursor latency to edit-commit latency.
Across the whole list, name the brand but defend the category under it. Stateful WebSocket servers so edits push live, a single in-memory owner so one canonical order exists, an append-only log so edits stay durable and replayable in order, an in-memory relay since presence can be lossy. The split worth naming is the one the design rests on. The edit path wants a durable, ordered, replayable log, since losing one operation corrupts the document, while the presence path wants a fast, lossy, in-memory relay that repairs itself on the next update. Say which writes are durable and ordered, which deliveries are best-effort, and where the boundary sits. Blur the two together and you get either a stuttering cursor or a corrupted document.
Potential deep dives
Most of the interview signal lives here, and for this prompt nearly all of it is about keeping copies convergent under concurrent and out-of-order edits. The interviewer picks one or two and pushes until two screens stop agreeing in your story. The five below are ordered the way an edit flows. How concurrent edits reconcile, how a client catches up after falling behind, how a user undoes only their own work, how cursors stay live without dragging on the edit path, and how the log plus snapshots keep reopening fast.
1) Resolving concurrent edits: operational transformation
Start with the question the design exists to answer, and work it from a concrete case. The text is "cat" at version 3. Editor A inserts "s" at position 0 to make "scat". At the same instant, Editor B deletes position 0 to make "at". Both edits were written against version 3, neither saw the other, and both are now headed for the Document Service. Apply them naively in arrival order and the two clients drift to different strings: one screen shows "cat" and the other shows "sat". That is the one outcome the product cannot ship. So what reconciles them?
A position-based edit is only valid against the exact text it was written against. "Delete the character at index 1" means something different once someone else has inserted a character ahead of index 1. Every approach to this problem is, underneath, one thing: a way to keep an edit meaningful after the document shifted beneath it. Say that before you say "OT" or "CRDT". The vocabulary is downstream of the idea.
Approach
Treat each save as a whole-document write and let the last one win, or lock the document so only one person edits at a time and everyone else waits or works read-only.
Challenges
Last-write-wins silently destroys the other person's work: two people type for a minute, one save lands second, and the other minute is gone. Locking technically converges, but it is not collaborative editing, it is a turnstile, and it fails the core requirement that several people type at once. Name them, say why they break the requirement, and move on to the real answer.
Approach
Each edit is a small operation, insert "s" at position 0, tagged with the version it was written against. The Document Service is the single authority that orders edits and transforms each incoming operation against the operations committed since the client's version. When B's "delete at 0" arrives after A's "insert at 0" has committed, the server shifts the delete to "delete at 1" so it still removes the original "c" in "scat" and leaves A's new "s" alone. Then it commits, bumps the version, and broadcasts. Every client runs the same transform against its own un-acknowledged local edits. Because all transforms route through one authority that fixes the order, the math stays tractable and operations stay tiny, just a type and a position plus one character.
Challenges
The transform functions are subtle and notoriously hard to get exactly right, especially once you go past plain text into formatting and overlapping ranges, and several published algorithms were later shown to be buggy on edge cases. It also leans on a central authority to impose order, so it is not a peer-to-peer or offline-first design without extra work. The payoff is compact operations and a model that scales to large documents through one server. A live document editor already has that shape.
Approach
Instead of transforming positions, give every character its own globally unique, immutable id that encodes a stable order, a fractional position between its neighbors plus the author's site id to break ties. An insert says "this character goes between id A and id B", which stays true no matter what else happened. There is nothing to transform. Edits commute: any two can be applied in either order and reach the same result. Merging two replicas is just taking the union of their operations. No central authority is required to decide order, which makes CRDTs the natural fit for peer-to-peer and offline-first editors where clients may sync hours apart.
Challenges
Every character carries its own id and tombstone metadata, so the in-memory and on-the-wire footprint is heavier than the terse operations OT sends, and deleted characters often linger as tombstones rather than truly disappearing. For a long document edited by a handful of people through one server, that overhead buys convergence you could already get more cheaply. CRDTs earn their keep when there is no central server to lean on. With one, the compact operations usually win.
Pick one and defend it. Both OT and CRDT solve convergence; they disagree on where the work goes. OT keeps operations tiny and pushes the complexity into transform functions and a central ordering server, and a server-mediated document editor already has one. CRDTs move the complexity into per-character metadata so no central authority is needed. That suits a peer-to-peer or offline system. This design is server-mediated, with one Document Service already owning each document, so operational transformation is the fit, and the metadata overhead of a CRDT would buy convergence the central order already provides. If the prompt drifts toward long offline editing or peer sync, that is the moment to switch your answer to a CRDT and say why. The strong answer commits to one approach for a server-mediated editor and can show the transform by hand.
2) Ordering and sync: the server as source of truth
Convergence rests on one thing: there is a single, canonical order of edits, and the Document Service decides it. A client's local copy is only a belief about the document, applied optimistically so typing feels instant. The server's committed sequence is the truth, and the version number is the lever that makes the gap between them exactly computable. Where this gets tested is the unhappy path. Connections drop. A laptop sleeps, wifi flickers, a phone changes towers. While a client is gone the document moves on, and when it reconnects it is some number of versions behind, possibly holding local edits the server never saw. The reconnect has to converge both directions without losing either side's work.
Approach
On any disconnect, throw away the client's state and re-fetch the latest snapshot fresh from the Document Store.
Challenges
It discards any edits the client made while briefly offline, which is data loss the moment a flaky network blips mid-sentence. And a full reload of a large document on every transient hiccup is wasteful when the client was usually only a few operations behind. Correct convergence needs the client's missed operations and its un-sent local operations both reconciled.
Approach
Every client tracks the version it last applied and the local operations it has made but not had acknowledged. On reconnect it tells the server "I'm at version N." The Document Service reads the Op Log forward from N and streams the missed operations. The client transforms those against its pending local operations and applies them, catching itself up. Then it re-sends its pending operations, now correctly based, and the server transforms and commits them against everything that happened while the client was away. Both sides converge, and only the delta crossed the wire. The version number on every operation is what makes the gap exactly "everything in the log after N." A short heartbeat over the socket detects the drop quickly, so the gap stays small.
Challenges
The client must hold its pending operations durably enough to survive the disconnect, in local storage. The server must retain log history far enough back to cover realistic offline gaps, which it does anyway for snapshots and undo. Genuinely long offline windows of hours or days stretch this into full offline-first editing, which is where a CRDT's no-central-order property starts to look attractive, and a fair place to say so.
The same version-replay machinery handles an owning Document Service instance dying. Because every committed edit was appended to the Op Log before it was acknowledged, a new owner rebuilds the live document from the last snapshot plus the log tail. Every client then reconnects and replays from its last-seen version, exactly as above. No acknowledged edit is lost, because the acknowledgment came after the append. Name that ordering, append then acknowledge, as the one invariant the whole design rests on. It separates a design that handles the happy path from one that survives a crash.
3) The undo and redo stack
Undo looks trivial until you put two people in the document, and then it is one of the sharpest tests in this prompt. The naive instinct is "undo removes the last edit." On a shared document that is wrong. The last edit in the Op Log very likely belongs to someone else, and pulling their change out from under them is a different kind of broken than the bug you were trying to fix. Undo has to be per-user: pressing undo walks back your last edit rather than whatever edit happened to land in the document most recently, even when a dozen other operations have committed on top of yours since.
Approach
Keep one undo stack for the document. Undo pops the most recent committed operation and reverses it; redo pushes it back.
Challenges
In a single-user editor this is fine. With several people it corrupts the shared mental model: you press undo and your collaborator's sentence vanishes, because their insert was the most recent operation. Two people pressing undo race on the same global stack and reverse each other's intent. The feature has to be scoped to the user who invoked it, and a single global stack has no notion of whose edit is whose.
Approach
Each Clients session keeps its own undo stack and redo stack of the operations that user has made. Pressing undo does not delete anything from the Op Log. It generates a new inverse operation that reverses only that user's own last edit, the inverse of "insert 's' at 3" being "delete at 3". That inverse is then transformed against everything committed since the original, by other people and by this user. That way it lands on the right character in the document as it stands now. Then it is appended and broadcast like any ordinary edit. The original edit moves to the user's redo stack; pressing redo replays its (re-transformed) effect the same way. So the document only ever moves forward by appending compensating edits, even as one user walks their own changes backward, and two people undo independently without colliding.
Challenges
The transform is the subtle part: an inverse computed against old positions has to be shifted to current positions before it applies, exactly the operational-transformation machinery from the first deep dive, reused. Edge cases need care, such as undoing an insert whose character a collaborator has since deleted, where the inverse becomes a no-op rather than an error. Keeping the per-user stacks bounded matters too, so a long session does not grow them without limit. The payoff is an undo that feels local and personal on top of a strictly append-only shared log.
4) Presence and cursors without dragging on the edit path
Presence looks like editing, small messages fanning out to collaborators, but its requirements are the opposite, and conflating the two is a common misstep. A cursor that lands a beat late is invisible. An edit that lands late or out of order corrupts the document. So presence gets a cheaper, separate path, and the discipline is refusing to hand it the edit path's guarantees.
Approach
Treat a cursor move as just another message on the durable, ordered edit stream, logged and broadcast like an operation through the Document Service.
Challenges
Cursors move far more often than text changes. The firehose of cursor updates floods the Op Log and the broadcast with ephemeral data nobody will ever replay, paying durable-write and strict-ordering cost for information that is worthless one update later. It also couples presence latency to edit-commit latency, so a momentarily slow edit path makes everyone's cursors stutter at once.
Approach
Run cursors and the "who is here" list through the Presence Service, an in-memory relay on Redis pub/sub that fans each update to the document's other sessions and stores nothing durably. Each session's presence is keyed by document with a short timeout, so a client that goes silent simply expires out of the banner, and a join or a cursor move refreshes it. Drop a message and the next update repairs the view within a fraction of a second. The edit path stays clean and strict, carrying only the text that has to converge, while the cheap path carries everything that does not.
Challenges
Presence becomes best-effort, and you should say that is deliberate. A missed final cursor or a slightly stale "still here" is fine because no document state depends on it. The one thing to get right is expiry, so a crashed client does not haunt the collaborator list forever, and the short timeout handles that. Cursor positions also have to ride along as the text shifts beneath them, transformed the same way edits are, so a collaborator's caret stays on the character it was next to rather than drifting.
5) Persistence and snapshots
Durability and fast reopening both fall out of the op log, and each needs a concrete mechanism. Size it first. A busy document might log a few thousand operations in a single editing session, and over its life a heavily revised document climbs into the millions. A fresh client cannot replay millions of operations to open the document. Snapshots are not optional. They keep open latency flat no matter how old the document is.
Approach
Every committed operation appends to the per-document Op Log in order. Opening a document replays the whole log from the start to rebuild the text. Durability is solid, nothing is overwritten, and the full history is always present for undo and the revision view.
Challenges
Open latency grows without bound as the log grows, because a million-operation document replays a million operations on every open. Pure log replay is correct but does not scale to a document's whole lifetime, and a returning collaborator should not wait seconds to see the text.
Approach
A snapshot worker periodically writes the full materialized text to the Document Store tagged with the version it covers, every few hundred operations or every few minutes for an active document. Opening a document loads the latest snapshot and replays only the operations logged after it, a bounded and small number, so open latency stays flat regardless of the document's age. Log records older than the newest snapshot can be tiered to cheap storage or trimmed once history past a horizon is no longer needed, while recent operations stay hot for fast catch-up. Load the snapshot, replay the tail, done.
Challenges
The cadence is a real tradeoff: snapshot too rarely and the trailing replay grows; snapshot too often and you spend storage and write bandwidth on redundant full copies. Tie the interval to operation count rather than wall-clock time, so a quiet document does not snapshot needlessly and a busy one snapshots enough. And undo and the revision view need history kept back far enough to cover them, which sets the floor on how aggressively the log is trimmed.
The clean way to say all of this: the Op Log is the source of truth and the Document Store is a cache of its result. Every guarantee in the design, durability, convergence, reconnect, undo, history, traces back to that ordered, append-only log, and snapshots exist purely so nobody pays to replay the log from the very beginning. Frame persistence that way and the snapshot cadence becomes an optimization on top of a correct foundation rather than a load-bearing hope.
Key Takeaways
- ✓Lead with convergence and the latency budget over raw scale. The whole product is that every screen ends on identical text after concurrent edits, and sub-100-millisecond propagation forces persistent connections pushing operations both ways. Say "the copies must converge" before drawing a single box.
- ✓Resolve concurrent edits with operational transformation through a single ordering point. A position-based edit is only valid against the text it was written on, so the Document Service transforms each incoming operation against everything committed since. Pick OT over a CRDT for a server-mediated editor and say why: the central order makes OT compact, while a CRDT only earns its metadata cost without a central server.
- ✓Make undo per-user. Pressing undo appends a transformed inverse of that user's own last edit rather than popping the shared log, so two people undo independently and the log stays strictly append-only. This is the Adobe-flavored subtlety the round pushes on.
- ✓Build everything on an append-only op log plus periodic snapshots, and append before you acknowledge. The log is the source of truth and gives durability, reconnect by version replay, undo, and history. Snapshots keep reopening fast. A separate lossy presence relay carries cursors, so the strict edit path pays only for the text that must converge.
Adobe interview guideKnow what Adobe asks before you walk in.
20 coding · 4 system design · 22 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.