Design Collaborative Editing: Real-Time Documents, Conflict Resolution, and Recovery

25 minute read

Published:

1. What Collaborative Editing Must Provide

Collaborative editing lets several people modify the same document at the same time and see each other’s changes quickly. A single-user editor has one local truth. A collaborative editor has many clients, many network paths, and one shared document that must remain understandable after concurrent edits.

Consider a document that currently contains:

The cat sat.

Two users edit at nearly the same time:

  • user A inserts black before cat;
  • user B deletes sat;
  • both users expect their cursor, undo history, and document view to remain sensible.

The core problem is not only low latency. The system must preserve a coherent document while clients optimistically edit local copies, messages arrive in different orders, users disconnect, and servers are replaced.

A collaborative editor usually separates several kinds of state:

StateExamplesDurability requirement
Document stateText, paragraphs, formatting, comments, titleDurable
Edit historyOrdered operations, revisions, snapshotsDurable enough for recovery and audit needs
Session stateActive connection, last acknowledged revision, selected rangesRecoverable
Presence stateCursor position, user color, typing indicatorEphemeral
Authorization stateWho can read, comment, suggest, or editDurable policy with current enforcement

The design goal is to let users edit locally for responsiveness while the service establishes one authoritative order for durable changes.

What to remember: Collaborative editing is not just WebSocket broadcast. It is optimistic local editing plus authoritative ordering, conflict handling, persistence, presence, reconnection, and permission enforcement.


2. Requirements

Assume we are designing an online document editor similar to a simple Google Docs-style product.

Functional Requirements

The functional surface has three layers: document operations, live collaboration, and access control.

AreaRequirement
Document lifecycleUsers can create, open, edit, and save documents.
Editing modelUsers can change text and basic rich-text attributes.
Concurrent editingMultiple users can edit the same document at the same time.
Live propagationAccepted edits appear on other clients with low delay.
PresenceUsers can see collaborators’ cursors and selections.
AnnotationsComments or suggestions are stored as durable document annotations.
RecoveryA reconnecting client can reload history and resume from persisted state.
PermissionsViewers, commenters, and editors have different capabilities.

Non-Functional Requirements

The non-functional requirements define the quality of the editing experience and the failure behavior.

PropertyRequirement
Local responsivenessThe author sees their own edit immediately, before the server round trip completes.
ConvergenceClients that receive the same accepted operations eventually render the same document.
Bounded resourcesDocument sessions, WebSocket connections, queues, and presence state have explicit limits.
Durable recoveryAcknowledged edits survive client, gateway, and collaboration-service failure.
Horizontal scaleThe system can spread documents, connections, and fanout across many servers.
OperabilityMetrics explain edit latency, conflicts, reconnects, fanout pressure, and storage lag.

Useful Simplifications

This article focuses on collaborative document editing, not a complete office suite. It does not design image embedding, spreadsheet formulas, offline mobile editing for weeks, legal hold, malware scanning, or full version-history UI. Those can be added later, but they should not obscure the central editing path.


3. The Document Model

A collaborative editor needs a representation that can be changed by small operations. The simplest model is a string:

document = "hello world"

Production editors often use a tree or sequence of blocks:

document
    block p1: paragraph
        text run: "hello"
        text run: " world" {bold: true}
    block p2: paragraph
        text run: "..."

The exact structure depends on product requirements, but the same ownership model appears in most designs:

OwnerResponsibility
ClientRender a local copy and create operations against the revision it has seen.
Collaboration serviceValidate, authorize, order, transform or merge, persist, and publish accepted operations.
Other clientsApply accepted operations in revision order until their local copies converge.

The service gives each durable document version a monotonically increasing revision:

document d1
revision 100: "hello world"
revision 101: insert "!" at position 11
revision 102: delete 1 character at position 5

A client usually tracks:

documentId
sessionId
userId
local document copy
server revision last applied
pending local operations not yet acknowledged
cursor and selection

Revision numbers give the system a shared language for causality. Operation op-17 based on revision 100 means the client created the operation while its known server state ended at revision 100.


4. High-Level Architecture

A practical collaborative editor has a durable document path and a live session path.

Collaborative editing architecture

One possible architecture is:

browser editor
    -> load balancer
    -> WebSocket gateway
    -> collaboration/session service
    -> operation log
    -> snapshot/document store
    -> pub/sub or stream
    -> other gateways with active users

The components have separate responsibilities:

ComponentResponsibility
Browser editorMaintains a local document copy, captures user edits, renders remote edits, and preserves cursor state.
WebSocket gatewayOwns live connections, heartbeats, backpressure, authentication context, and message framing.
Collaboration serviceValidates edit permissions, orders operations for a document, transforms or merges concurrent operations, and emits accepted revisions.
Operation logStores accepted operations in revision order for replay, audit, and recovery.
Snapshot storeStores compact document snapshots so opening a document does not replay infinite history.
Pub/sub streamCarries accepted operations from the document owner to gateways that have subscribed clients.
Presence serviceTracks ephemeral cursors, selections, and active users.
Auth serviceAnswers who can read, comment, suggest, or edit the document.

The WebSocket gateway should not be the only durable source of document state. It owns live sockets. The collaboration service and storage own the document’s accepted history.


5. Opening a Document Session

When a user opens a document, the system needs both durable state and live routing state.

A typical open flow is:

1. Browser requests document d1.
2. Service authenticates the user and checks read permission.
3. Browser loads a snapshot at revision R.
4. Browser opens a WebSocket session for document d1.
5. Gateway authenticates the socket and subscribes to document d1 updates.
6. Client asks for operations after revision R.
7. Client applies missed operations and becomes current.
8. Presence service announces the user's cursor and selection.

The snapshot and operation replay step matters. A document may have changed between the HTTP snapshot load and WebSocket subscription. The client therefore needs a catch-up boundary:

loaded snapshot revision: 250
latest accepted revision: 256
client applies:          251..256

After catch-up, the client can apply new accepted operations as they arrive. This avoids a race where the page renders revision 250, subscribes too late, and silently misses revision 251.


6. The Editing Path

For low perceived latency, the client normally applies the user’s edit optimistically before the server round trip completes.

Suppose the current document at revision 100 is:

hello world

User A types ! at the end. The browser immediately renders:

hello world!

and sends an operation:

{
  "type": "insert",
  "documentId": "d1",
  "clientId": "cA",
  "operationId": "a17",
  "baseRevision": 100,
  "position": 11,
  "text": "!"
}

The server path is:

receive operation
    -> authenticate session
    -> authorize edit
    -> validate operation shape and size
    -> transform or merge against operations after baseRevision
    -> assign next document revision
    -> persist operation
    -> acknowledge author
    -> publish to other subscribed sessions

One collaborative edit from local operation to accepted revision

The acknowledgement tells the author that its local speculative operation is now part of the authoritative history:

{
  "type": "ack",
  "operationId": "a17",
  "assignedRevision": 101
}

Other clients receive the accepted operation:

{
  "type": "remote_operation",
  "documentId": "d1",
  "revision": 101,
  "operation": {
    "type": "insert",
    "position": 11,
    "text": "!"
  }
}

This is not ordinary message broadcast. The operation must be accepted in a single document order. If every gateway independently broadcasts edits, clients can apply operations in different orders and diverge.

What to remember: The client can be optimistic, but the document needs an authoritative operation order.


7. Concurrent Edits and Operational Transformation

Concurrent edits are edits created by clients that have not yet seen each other’s operations. The system must preserve each user’s intention as much as the data type allows.

Operational transformation, or OT, is one common technique. In an OT system, operations are transformed against concurrent operations so they can be applied to a newer document state.

Given a text document with string abc, two users create concurrent operations:

O1 = Insert(position=0, text="x")
O2 = Delete(position=2, text="c")

If O1 is applied first, the document becomes:

xabc

The original O2 says “delete at position 2.” On the new document, position 2 contains b, not c. To preserve the intent of deleting c, the server transforms O2 against O1:

O2' = Delete(position=3, text="c")

Applying O2' to xabc produces:

xab



The simplified server rule is:

incoming operation is based on revision B
current document revision is R
if B < R:
    transform incoming operation against accepted operations B+1..R
assign revision R+1
persist and publish

For simple text:

Concurrent pairTypical transformation intuition
Insert before insertLater operation’s position may shift right.
Insert before deleteDelete position may shift right.
Delete before insertInsert position may shift left.
Delete before deleteDeleting the same range may collapse or become a no-op.

Real editors handle more than characters: paragraphs, attributes, comments, tables, embeds, undo, redo, and selections. Each operation type needs precise transformation rules and tests for convergence.

Server-Side Ordering

Many production OT designs use a central sequencer per document or shard. The sequencer decides the next revision for a document:

op from client A at base 100
op from client B at base 100

sequencer chooses:
revision 101 = transformed A
revision 102 = B transformed against revision 101

A document sequencer orders concurrent operations and transforms the later one

This makes reasoning easier because every accepted operation has one document revision. The trade-off is that all edits for one hot document pass through the same ordering point. That is usually acceptable for documents because one document’s human edit rate is modest compared with system-wide traffic.

Client-Side Pending Operations

While waiting for acknowledgement, the client may have local pending operations. When a remote operation arrives, the client applies it carefully:

server document at revision 100
client applies local pending op A
remote accepted op B arrives as revision 101
client transforms B against pending A for local display
client also transforms pending A against B for future acknowledgement

This keeps the screen responsive while preserving convergence with the server’s accepted order.


8. OT Versus CRDTs

Operational transformation is not the only approach. Conflict-free replicated data types, or CRDTs, represent document state so concurrent updates can be merged without a single central transformation point.

The high-level trade-off is:

ApproachGood fitMain cost
OT with server orderingOnline editors with a central service, revision history, and relatively short offline windows.Transformation rules are subtle and every operation type must be correct.
Sequence CRDTLocal-first or offline-heavy editors where replicas may accept edits independently and merge later.Metadata, tombstones, ordering identifiers, and compaction can become complex.

In a system-design interview or architecture document, it is usually enough to choose one model and explain the consequences. This post uses OT with a server-assigned document order because it matches many browser-based collaborative editors and keeps the durable history easy to explain.

The same surrounding architecture still matters with CRDTs: authentication, presence, WebSocket gateways, storage, replay, snapshots, backpressure, and observability do not disappear.


9. Persistence: Operation Log and Snapshots

The operation log is the durable source of accepted changes:

documentId=d1
revision=101
operation=insert("!", 11)
author=userA
timestamp=...

The document can be reconstructed by applying operations in order to a previous snapshot:

snapshot at revision 100
    + operations 101..250
    -> document at revision 250

Snapshots accelerate load while the operation log remains authoritative

Replaying from revision zero forever is expensive, so the system periodically stores snapshots:

snapshot revision 1000
snapshot revision 2000
snapshot revision 3000

Opening a document then becomes:

load latest snapshot <= requested revision
load operation suffix after that snapshot
apply suffix
return document and current revision

Snapshot creation must be tied to the operation log position. A snapshot that claims revision 3000 must include every accepted operation through 3000 and none after it. Otherwise clients can miss or duplicate changes during load and replay.

Storage Choices

Common storage layout:

DataStorage pattern
Document metadataRelational database or strongly consistent key-value store.
Operation logAppend-friendly store partitioned by document ID.
SnapshotsObject storage or document database keyed by document ID and revision.
PresenceIn-memory store with TTLs or gateway-local state.
Connection directoryRedis-like store, service registry, or partitioned in-memory service with leases.

The operation log should support ordered reads by document and idempotent writes. If the client retries operation a17, the service should return the existing result rather than applying the same edit twice:

(documentId, clientId, operationId) -> assigned revision

10. Presence, Cursors, and Selections

Presence is usually not part of the durable document history. A cursor update is useful for collaboration, but it does not need to survive a server crash in the same way as inserted text.

A presence message might look like:

{
  "type": "presence",
  "documentId": "d1",
  "sessionId": "tab-8",
  "cursor": 42,
  "selection": [42, 51],
  "lastSeenRevision": 256
}

Presence has different rules from durable edits:

Durable editPresence update
Persisted before acknowledgement.Usually best-effort.
Ordered by document revision.May be throttled, sampled, or overwritten.
Replayed after reconnect.Recreated by active clients.
Requires edit permission.Requires at least document visibility.

Cursor positions also need transformation. If user B’s cursor is after a range where user A inserts text, B’s displayed cursor should move. Many editors transform selections using the same operation stream used for document edits.


11. WebSocket Connections and Reconnection

WebSockets are a good fit for collaborative editing because the server often needs to push accepted operations and presence updates without waiting for the browser to poll.

The WebSocket connection is still a disposable transport path. It can last for hours, but it can also disappear because a laptop sleeps, a phone changes from Wi-Fi to mobile data, a NAT mapping expires, a load balancer closes an idle connection, or a gateway is deployed.

The server maintains liveness with heartbeats:

gateway sends ping or application heartbeat
client responds with pong or heartbeat_ack
gateway closes after missed deadlines

When the client reconnects, it should not ask for “whatever is current” without context. It should resume from the last durable document revision it processed:

{
  "type": "resume",
  "documentId": "d1",
  "sessionId": "tab-8",
  "lastAppliedRevision": 256,
  "pendingClientOperations": ["a19", "a20"]
}

The server responds with:

acknowledged pending operations
rejected or unknown pending operations
accepted operations after revision 256
current presence snapshot

Reconnect creates a new WebSocket and resumes from the last applied revision

If the client changed IP address, the old TCP connection cannot be moved to the new network path. The client creates a new connection, authenticates again, and resumes application state. From the document’s point of view, continuity comes from sessionId, operation IDs, and revision replay, not from the old socket.


12. Authentication and Authorization

Authentication identifies the user. Authorization decides what that user may do to a specific document.

The system checks permissions at several points:

MomentRequired check
Load snapshotUser can read the document.
Open WebSocket sessionUser can join this document session.
Submit editUser can edit or suggest at this revision.
Add commentUser can comment.
Subscribe to presenceUser can see collaborators.
Permission changesExisting sessions may need to be downgraded or closed.

Long-lived connections need current authorization. If user A loses edit access while their WebSocket remains open, the next edit must be rejected even though the connection authenticated successfully an hour ago.

Common patterns:

PatternWhy it matters
Authenticate the WebSocket handshake with a secure cookie or short-lived connection ticket.The gateway can reject unauthorized sessions before accepting long-lived work.
Attach a server-side session identity to the connection object.Later operations can be checked without trusting client-supplied user IDs.
Authorize every document operation against current policy or a bounded policy snapshot.Long-lived sockets do not become permanent permission grants.
Notify, downgrade, or disconnect sessions when document permissions change.Existing editors stop acting on stale privileges.
Make operation submission idempotent.Retries with the same operation ID do not duplicate edits.

Authentication belongs to the session. Authorization belongs to each protected action.


13. Scaling the System

The system scales along several dimensions.

Many Connections

WebSocket gateways hold many mostly idle connections. A gateway usually uses non-blocking I/O and event loops rather than one thread per connection. Each connection has bounded input buffers, output queues, heartbeat timers, and subscription state.

Backpressure matters. If one client cannot receive updates quickly, the gateway must not keep unlimited messages in memory. It can disconnect the client, coalesce presence updates, or rely on durable replay for document edits.

Many Documents

Documents can be partitioned by documentId:

hash(documentId) -> collaboration shard

One active document should normally have one authoritative operation sequencer at a time. That sequencer can be a collaboration service instance, an actor, a partition in a stream, or a lease holder. The important property is that accepted revisions for one document are assigned in one order.

Popular documents create fanout pressure. If 20,000 viewers watch one document, the service should publish one accepted operation to each gateway with local subscribers, then let those gateways fan out locally.

collaboration service
    -> gateway A has 800 viewers
    -> gateway B has 1200 viewers
    -> gateway C has 500 viewers

A hot document is ordered once and then fanned out by gateways

Sending one message per viewer through the central service wastes work and can make one popular document affect unrelated documents.

Many Regions

Multi-region collaboration is harder than read-only document serving. The system must choose where a document is actively sequenced.

Common choices:

ModelBehavior
Single active region per documentEasier ordering and conflict handling; remote users pay extra latency.
Region near document ownerGood for teams clustered around one geography.
Dynamic document leaderCan move active sequencing, but migration needs careful fencing.
Multi-leader CRDTBetter offline and regional autonomy; more metadata and merge complexity.

For an OT design, a single active sequencer per document is the simpler default. The global system can still route WebSockets to nearby gateways; those gateways forward edits to the document’s current sequencer.


14. Failure Handling

Failures are normal. The design should state what survives, what is retried, and what is reconstructed.

FailureExpected behavior
Browser tab closesGateway removes session and presence; durable edits already acknowledged remain in the log.
Client network changesOld socket fails; client reconnects, authenticates, and resumes from last applied revision.
WebSocket gateway crashesLive connections disappear; clients reconnect to other gateways; presence is recreated; durable operations come from log.
Collaboration sequencer crashesA new owner recovers from operation log and latest committed revision before accepting edits.
Pub/sub delayGateways may lag; clients catch up using revision replay.
Snapshot writer failsOperation log remains authoritative; future snapshot can retry.
Storage write failsEdit is not acknowledged as durable; client retries with same operation ID.
Permission changesExisting sessions receive updated capability, rejected commands, or forced disconnect.

Fencing the Document Owner

If one document has a current sequencer, failover must prevent two sequencers from accepting competing revisions. A lease or term can fence old owners:

owner term 41 accepts revisions 900..940
owner term 42 takes over after failure
late write from term 41 is rejected

Every accepted operation is written with the owner term and next revision. The storage layer rejects stale terms or duplicate revisions.

Reconnect Storms

A gateway restart can cause thousands of clients to reconnect at once. Clients use exponential backoff with jitter. Gateways bound concurrent handshakes, authentication calls, document catch-up reads, and subscription registration.

The system should be tested by killing a gateway, restarting a collaboration shard, delaying the pub/sub stream, and forcing clients to change networks.


15. Capacity and Backpressure

Capacity is more than the number of open documents. The workload has live connection cost, document-ordering cost, storage cost, and recovery cost.

DimensionWhy it matters
Concurrent WebSocket connectionsDrives gateway memory, file descriptors, heartbeats, and load-balancer state.
Active editing sessions per documentDrives conflict rate, transform work, and per-document coordination.
Operation rate and sizeDrives validation, transformation, log writes, publish volume, and bandwidth.
Fanout recipients per operationTurns one accepted edit into many gateway deliveries.
Queued output per connection and gatewaySlow clients can convert fanout into memory pressure.
Operation-log write throughputAccepted edits cannot be acknowledged durably without this path.
Catch-up reads after reconnectGateway restarts and network changes can create replay bursts.
Snapshot size and frequencyControls open latency and replay length.
Presence update rateCursor movement can dominate message volume if not throttled.
Auth and authorization lookup rateReconnect storms and permission checks can overload dependencies.

For a rough fanout estimate:

outbound messages per second ~= accepted operations per second * subscribed sessions

If one document receives 20 accepted operations per second and has 5,000 subscribed sessions:

20 * 5,000 = 100,000 outbound operation deliveries per second

Presence can be even noisier because cursor movement produces frequent updates. Presence should be throttled, coalesced, and treated as best-effort. Durable document edits should be persisted and replayable.

Backpressure policy should be data-specific:

Data typeBackpressure policy
Text operationPersist and replay; disconnect slow clients if queues exceed limits.
Cursor updateDrop old values and send the latest.
Typing indicatorDrop freely; it expires.
Comment creationPersist and acknowledge only after durable write.
Large pasteBound size, chunk if needed, or reject with clear error.

16. Observability

Useful metrics should preserve the stages of the system:

StageUseful signals
Open and connectOpen document latency, WebSocket connection count by gateway, reconnect rate, resume success rate.
EditingActive editors and viewers by document, operation submit latency, transform latency, conflict rate.
PersistenceOperation-log append latency, snapshot age, replay length, duplicate operation rate.
DeliveryPublish-to-gateway latency, gateway output queue bytes, client acknowledgement lag.
PresencePresence update rate, coalescing rate, drop rate, stale presence count.
AuthorizationPermission rejection rate, permission-change propagation latency.
RecoveryOperation replay count after reconnect, sequencer failover count and duration.

Useful logs include:

documentId
userId or anonymized principal
sessionId
operationId
baseRevision
assignedRevision
sequencer term
gatewayId
latency by stage
result: accepted | transformed | rejected | duplicate | stale_base

The most useful incident view follows one operation end to end:

client creates op
    -> gateway receives it
    -> collaboration service orders it
    -> operation log persists it
    -> pub/sub delivers it
    -> gateways fan out
    -> clients acknowledge applied revision

A fleet-wide average can hide one hot document, one slow gateway, or one partition with high transform latency.


17. When Collaborative Editing Is the Wrong Tool

Collaborative editing infrastructure is expensive compared with ordinary save and reload flows. Use it when concurrent work is central to the product.

NeedSimpler approach
Rare edits by one user at a timeLock document while editing or use last-write-wins with version checks.
Forms with independent fieldsField-level optimistic concurrency may be enough.
Read-mostly document with occasional commentsOrdinary HTTP plus comment refresh or notifications.
Presence-only experienceWebSocket presence without real-time document mutation.
Offline-first peer collaborationConsider CRDTs and local-first storage rather than central OT.

The design should match the collaboration semantics the product actually needs.


18. Complete Mental Model

A collaborative editor combines several separate mechanisms:

local editor
    -> optimistic operation
    -> WebSocket gateway
    -> document sequencer
    -> transform or merge
    -> durable operation log
    -> publish accepted revision
    -> gateway fanout
    -> client apply and acknowledge

The durable path decides what the document is. The live path decides who sees changes quickly. The recovery path reconstructs state after disconnection.

The most important design boundaries are:

BoundaryPractical meaning
WebSocket connection versus sessionThe socket is transport; durable session state must survive reconnect.
Client arrival order versus document revisionArrival order is incidental; accepted revision order defines the document.
Presence versus document editsPresence is ephemeral; edits are durable and replayable.
Authentication versus authorizationAuthentication identifies the session; authorization protects each action.
Retry versus duplicate editOperation IDs make repeated submissions idempotent.
Snapshot versus historySnapshots accelerate load; the operation log defines accepted history.
Document owner versus global fleetOne active sequencer per document simplifies OT while the fleet scales across documents.
Reconnect versus recoveryReconnect creates transport; revision replay reconstructs document continuity.
Slow client versus system healthBounded queues and data-specific backpressure prevent one client from harming the session.

After reading the post, the practical design question should be clear:

For each document, who orders edits?
For each operation, how is intent preserved?
For each client, how is local optimism reconciled with server authority?
For each disconnect, what state lets the session resume?
For each failure, which component owns recovery?

References

  1. C. A. Ellis and S. J. Gibbs, “Concurrency Control in Groupware Systems”
  2. C. Sun and C. Ellis, “Operational Transformation in Real-Time Group Editors”
  3. M. Shapiro et al., “Conflict-Free Replicated Data Types”

Leave a Comment