Markdown Editor

Collaborative Editing System Design Template (Reference Design)

Real-time co-editing: OT vs CRDT, sequence CRDT, state-vector sync, presence, offline merge, snapshots and compaction, access

Mở trong editorTải .md

Cách dùng: Use when adding multi-user or offline-capable editing. Decide the document model and offline requirement before choosing OT or CRDT. The main pitfall is ignoring that revoked users keep offline replicas and that tombstones grow.

Xem trước

System Design: [Collaborative editor name]

Reference design for real-time collaborative document editing: several people edit the same document at once, see each other's cursors, keep working offline and merge cleanly when they reconnect. It compares operational transformation (OT) with conflict-free replicated data types (CRDTs), chooses a sequence CRDT, and specifies the sync protocol, presence, offline merge, persistence with snapshots and compaction, access control and local-first considerations. It follows the structure of the System Design Document template and is pre-filled with a worked design to adapt. Values marked Example are assumptions.

Document control

FieldValue
Document ID[SDD-COLLAB-001]
System / feature[Real-time collaborative editing]
Version[0.1]
Status[Draft / In review / Approved / Implemented / Superseded]
Author(s)[@author]
Reviewers[@reviewer-1], [@reviewer-2]
Last updatedYYYY-MM-DD
Related[PRD], [ADR: CRDT library], [Threat model], [Offline storage design]

Approvals

RoleNameDecisionDate
Tech lead[@name][Approved / Changes requested]YYYY-MM-DD
Staff / principal engineer[@name][Decision]YYYY-MM-DD
Editor / client lead[@name][Decision]YYYY-MM-DD
SRE / operations[@name][Decision]YYYY-MM-DD
Security and privacy[@name][Decision]YYYY-MM-DD

Revision history

VersionDateAuthorChange summary
0.1YYYY-MM-DD[@name]First draft for design review
0.2YYYY-MM-DD[@name]Example: Chose sequence CRDT over server OT to support offline editing
0.3YYYY-MM-DD[@name]Example: Added snapshot compaction and history retention policy
1.0YYYY-MM-DD[@name]Example: Approved

Contents

1. Overview

1.1 Problem statement

[Example: Our editor saves whole documents with last-write-wins. When two people edit the same document, one person's changes disappear; support receives about 40 "lost work" tickets a week. Users also want to keep writing on trains and planes and sync later.]

1.2 Proposed solution

[Example: Each document is a sequence CRDT stored on every client in local storage. Edits apply locally at once and are encoded as small binary updates. Clients exchange updates through a sync server over WebSocket; the server authenticates, checks permissions, relays updates to other clients editing the same document, and appends them to a durable update log. Periodically the server merges the log into a compacted snapshot. Cursors and selections travel on a separate ephemeral presence channel. Because merges are deterministic on every replica, clients that were offline for days converge with everyone else on reconnect without server-side conflict resolution.]

1.3 Decisions requested from reviewers

IDQuestionRecommended optionSection
Q-01Example: OT or CRDT?Sequence CRDT (YATA or Fugue family) via a mature library7.1, 10
Q-02Example: Is the server a peer that merges, or a relay?Relay plus persistence; merges only for compaction7.2
Q-03Example: History retention?Named versions forever; fine-grained history 30 days, then compacted7.6
Q-04Example: End-to-end encryption?Not in v1; design keeps the server content-agnostic except for compaction7.7
[Q-nn][Question][Option][Section]

2. Requirements

2.1 Functional requirements

IDRequirementPriority
FR-01Example: Concurrent editing of the same document by up to 50 active editorsMust
FR-02Example: Remote edits appear without disrupting the local cursorMust
FR-03Example: Presence: who is here, their cursor and selection, with colour and nameMust
FR-04Example: Offline editing for any duration, merged automatically on reconnectMust
FR-05Example: Version history with named versions and restoreShould
FR-06Example: Roles: owner, editor, commenter, viewer; changes take effect immediately onlineMust
FR-07Example: Undo and redo affect only the user's own changesMust
[FR-nn][Requirement][Must / Should / Could]

2.2 Non-functional requirements

IDQualityTargetMeasured as
NFR-01Local latencyExample: Keystroke to local render at or below 16 msClient performance traces
NFR-02Remote latencyExample: Edit visible to other online editors within 200 ms p95 (same region)Sampled end-to-end probes
NFR-03ConvergenceExample: All replicas identical after exchanging all updatesProperty-based tests, checksum on sync
NFR-04DurabilityExample: An update acknowledged by the server is never lostReplicated log, restore drills
NFR-05Intent preservationExample: Concurrent inserts at the same position never interleave character by characterFuzz tests with known anomaly cases
NFR-06Document sizeExample: Smooth editing up to 1 MB of text and 1 million lifetime operationsBenchmark
[NFR-nn][Quality][Target][Measurement]

2.3 Out of scope

  • [Example: spreadsheets and drawing canvases (other data types later)]
  • [Example: comments and suggestions mode (next design, reusing the sync layer)]
  • [Example: peer-to-peer sync without a server]
  • [Item deferred, with the release that will take it]

3. Capacity estimation

3.1 Assumptions

InputSymbolValueSource
Concurrent editing sessions at peakCCExample: 500 000Product analytics
Average sessions per open documentnnExample: 2.5Product analytics
Updates per session per second (batched every 50 ms while typing)uuExample: 1 on averageClient telemetry
Encoded update sizesus_uExample: 100 bytesLibrary benchmark
Presence messages per session per second (throttled)ppExample: 2Client setting
Total documentsNNExample: 100 millionStorage inventory
Average compacted snapshotsds_dExample: 50 KBSample of documents

3.2 Update traffic

Open documents and inbound updates:

Dopen=Cn=5×1052.5=200 000 documents,Uin=C×u=500 000 updates/sD_{open} = \frac{C}{n} = \frac{5 \times 10^{5}}{2.5} = 200\,000 \ \text{documents}, \qquad U_{in} = C \times u = 500\,000 \ \text{updates/s}

Each update is relayed to the other n−1n - 1 sessions on average:

Uout=Uin×(n−1)=500 000×1.5=750 000 updates/sU_{out} = U_{in} \times (n - 1) = 500\,000 \times 1.5 = 750\,000 \ \text{updates/s}
Bin=Uin×su=50 MB/s,Bout=75 MB/sB_{in} = U_{in} \times s_u = 50 \ \text{MB/s}, \qquad B_{out} = 75 \ \text{MB/s}

Presence adds C×p×(n−1)=1.5×106C \times p \times (n-1) = 1.5 \times 10^{6} small relayed messages per second, never persisted.

3.3 Storage

Ssnap=N×sd=108×50 KB=5 TBS_{snap} = N \times s_d = 10^{8} \times 50 \ \text{KB} = 5 \ \text{TB}

Update log before compaction, if all inbound updates were kept for one day:

Slog/day=Uin,avg×su×86 400S_{log/day} = U_{in, avg} \times s_u \times 86\,400

With an average of one third of peak, Uin,avg≈170 000U_{in, avg} \approx 170\,000/s, so about 170 000×100×86 400≈1.5170\,000 \times 100 \times 86\,400 \approx 1.5 TB/day before compaction. Compaction (7.6) keeps the steady-state log small.

3.4 Summary

QuantityEstimateDesign consequence
Open documents~200 000 at peakRoute all sessions of a document to one sync node
Updates~500 000/s in, ~750 000/s outIn-memory relay per document; batched log appends
Presence~1.5 million messages/sSeparate ephemeral channel, throttled
Storage~5 TB snapshots, ~1.5 TB/day raw logContinuous compaction into snapshots

4. API design

4.1 Endpoints and messages

ChannelMessagePurposeIdempotent
WebSocket /v1/docs/{id}/syncsync1 (client to server)Client state vector: what it already hasYes
WebSocketsync2 (server to client)Updates the client is missing, plus server state vectorYes, applying twice is harmless
WebSocketupdate (both ways)Incremental CRDT updateYes, CRDT updates are idempotent
WebSocketack (server to client)Update durably stored up to client clockYes
WebSocketawarenessCursor, selection, user info; ephemeralLast-write-wins per client
HTTPS GET/v1/docs/{id}/versionsNamed versions and history pointsYes
HTTPS POST/v1/docs/{id}/versionsCreate named versionWith Idempotency-Key
HTTPS PUT/v1/docs/{id}/permissions/{principal}Change roleYes

4.2 Message examples

Messages are binary in production; the JSON below shows their fields. The state vector maps each client ID to the number of operations seen from it:

{"type": "sync1", "docId": "d_41K", "stateVector": {"1739201": 1820, "2210988": 455}}
{"type": "sync2", "docId": "d_41K", "update": "<binary: missing operations>", "stateVector": {"1739201": 1820, "2210988": 512, "3301207": 77}}
{"type": "awareness", "clientId": 2210988, "user": {"name": "Linh", "color": "#2f6fdd"}, "cursor": {"anchor": "<relative position>", "head": "<relative position>"}}

4.3 Errors and limits

CodeMeaningClient action
forbiddenRole does not allow writingSwitch editor to read-only; keep local changes as a private draft
doc-too-largeSnapshot exceeds limitShow warning; suggest splitting
rate-limitedToo many updates per secondBatch more aggressively
reauth-requiredSession token expiredRefresh token, reconnect, resync

5. Data model

5.1 Entities

Server-side, a document is a snapshot plus an append-only log of updates since the snapshot. Each client holds the same structure in local storage.

5.2 Storage choices

DataStoreKeyMain queriesWhy
Update logExample: Wide-column or key-value store with ordered keys(doc_id, seq)Append; range after seqHigh append rate, sequential reads per document
SnapshotsObject storage, metadata in database(doc_id, up_to_seq)Latest snapshotLarge blobs, cheap
Documents, permissions, versionsRelational databasedoc_idPermission check; listingsTransactions, queries
Client replicaIndexedDB (web), SQLite (mobile, desktop)doc_idLoad on open; append updatesOffline-first
PresenceIn memory on the sync nodedoc_id, client_idBroadcast to roomEphemeral

6. High-level design

All sessions of a document are routed to the same sync node, which holds the document's in-memory room.

ComponentResponsibilityScales by
Client CRDT replicaApply local edits, encode updates, merge remote updates, persist locallyPer device
RouterSend every connection for doc_id to its owning sync nodeConsistent hashing with bounded load
Sync nodeAuthenticate, authorise, relay updates and awareness, append to log, acknowledgeDocuments per node
Compaction workersMerge log into snapshots, garbage-collect where safeQueue of documents over thresholds
Document APIListing, permissions, versions, exportStateless replicas
[Component][Responsibility][Scaling unit]

7. Detailed design

7.1 OT versus CRDT

AspectOperational transformationSequence CRDT
Core ideaTransform concurrent operations against each other so they apply in any orderGive every element a unique, ordered identity so merges commute
Server roleUsually central server orders operationsAny replica can merge; server optional for correctness
Offline editingLong offline periods mean large transformation chains and frequent manual conflictsNatural: merge any time, any order
Correctness proofsTransformation functions are notoriously hard to get right for rich textConvergence follows from the data type's design; libraries are fuzz-tested
Metadata overheadSmall document stateElement IDs and tombstones; mitigated by run-length encoding and compaction
Maturity for rich textLong production history in centralised editorsMature open-source libraries with editor bindings

The design chooses a sequence CRDT because offline editing (FR-04) and local-first storage are requirements, and because it removes the need for a single ordering server per document.

7.2 Sync protocol (critical path)

Sync is a state-vector exchange followed by incremental updates. The same protocol handles first open, reconnect after a network drop and reconnect after weeks offline.

The client keeps every local update in its local store until the server acknowledges it, so closing the tab before sync never loses work. Relaying happens before or in parallel with the log append so that remote latency is not bounded by disk; the ack that lets the client drop its pending copy is sent only after the append is durable.

7.3 CRDT choice and document model

  • Text: a sequence CRDT from the YATA or Fugue family, as implemented by a mature library (for example Yjs, or Automerge for JSON-like documents). Prefer an algorithm that avoids interleaving anomalies when two users insert at the same position concurrently (NFR-05).
  • Rich text or structure: a tree of maps and sequences (paragraphs as sequence elements, attributes as maps) bound to the editor's schema through the library's editor binding.
  • Markdown sources: keep the Markdown text itself as the replicated sequence, so the source stays canonical and the preview is derived.
  • Identity: each client session gets a random client ID (32-bit or wider); operations are identified by (client_id, clock). Reusing a client ID across devices is forbidden because it breaks uniqueness.

7.4 Merge semantics users will see

SituationResult
Two users type at different placesBoth edits kept, no visible conflict
Two users type at the same positionBoth insertions kept as contiguous runs, ordered deterministically
One user deletes a paragraph while another edits inside itDeletion wins for the deleted characters; inserted characters survive as a fragment (library dependent), visible to both
Concurrent formatting of the same rangeAttribute merge per character; last writer by operation ID wins per attribute
Offline user edits a document for a weekMerges on reconnect; history shows their changes grouped by author

Undo is scoped to the local user's own operations (an undo manager that only tracks local origins), so pressing undo never removes a collaborator's text.

7.5 Presence and cursors

Cursors are stored as relative positions (anchored to element IDs, not character offsets) so they stay correct while text changes around them. Awareness messages are throttled to 2 per second per client, carry a 30 s timeout, and are never written to the log. When a client disconnects, its presence is removed after the timeout or immediately on a clean close.

7.6 Persistence, snapshots and compaction

  • The sync node appends updates in batches (every 20 ms or 64 KB) to the log under (doc_id, seq).
  • A document is queued for compaction when its log exceeds Example: 1 000 updates or 1 MB. The worker loads the latest snapshot, applies the log, writes a new snapshot with its state vector, then deletes log entries up to up_to_seq.
  • Fine-grained history (who typed what, when) is kept for 30 days through periodic history snapshots; named versions are kept indefinitely.
  • Tombstone garbage collection removes deleted content's payload while keeping the IDs needed for merging. Full removal of deleted-element IDs is only safe when no replica can still send operations referencing them, which cannot be known for offline clients; the design therefore keeps the IDs and relies on run-length encoding to keep them small.

7.7 Access control, consistency and local-first considerations

  • Consistency model: strong eventual consistency. Any two replicas that have received the same set of updates are identical, regardless of order. There is no global order and no locking; reads are always local.
  • Write authorisation: the sync node checks the role on connect and on every update. Updates from viewers are rejected, and the client is told to switch to read-only, keeping its local changes as a private draft rather than discarding them.
  • Revocation: removing access stops future sync immediately, but a replica already on the user's device keeps the content it had. State this in the sharing UI; it is a property of every offline-capable editor.
  • Server-side deletion of a document deletes snapshots and log; clients receive a tombstone on next connect and remove their replica after confirming with the user that unsynced work exists.
  • Local-first: the editor opens and works with no network; the server adds sharing, backup and multi-device sync, but is not required to edit.
  • End-to-end encryption (future): the protocol already treats updates as opaque bytes except during compaction. With E2EE, compaction moves to clients, which upload encrypted snapshots.

7.8 Client connection states

Clients expose their sync state in the UI so users know whether their work is on the server.

8. Scalability and performance

ConcernApproachLimit before redesign
Open documentsConsistent hashing of rooms to sync nodesExample: ~5 000 active rooms per node, ~40 nodes at peak with headroom
Very popular document (company all-hands notes)Room handles hundreds of editors; viewers receive batched updates every 250 ms~500 concurrent editors per room
Large documentsLazy loading of sub-documents (one per section or page)Single sequence above ~5 MB
Log write rateBatched appends per roomStore write throughput per partition
Reconnect storm after node restartRoom state reloaded from snapshot + log; jittered reconnectSnapshot load time

Latency budget for a remote edit (target p95 200 ms same region):

StepBudget
Client batching interval50 ms
Client to sync node40 ms
Authorisation check (cached role)1 ms
Relay to other clients40 ms
Merge and render on receiver10 ms
Headroom59 ms

9. Reliability, security and operations

9.1 Failure modes

FailureImpactDetectionMitigation
Sync node crashRooms on that node drop; clients reconnectConnection drop rateClients keep pending updates; router moves rooms; state rebuilt from snapshot + log
Two nodes host the same room (routing change)Updates split between nodes temporarilyRoom ownership lease conflictCRDT merge makes split harmless; both append to the same log; lease resolves ownership
Corrupted updateClient cannot apply; document may fail to loadDecode errorsValidate on receipt; quarantine bad updates; restore from snapshot
Compaction bugSnapshot differs from log replayChecksum comparison of replay vs. snapshotKeep log until snapshot verified; roll back to previous snapshot
Client local store evicted by browserUnsynced work lostPending-update age telemetryRequest persistent storage; warn when offline changes are unsynced for long
[Failure][Impact][Signal][Mitigation]

9.2 Security and privacy

ThreatControl
Unauthorised editsRole checked on connect and every update; server rejects viewer updates
Forged authorship (spoofed client ID)Server records authenticated user per update; history shows server-recorded author
Malicious oversized or crafted updatesSize limits, decode in a guarded path, per-connection rate limits
Data on lost devicesLocal store encryption where the platform supports it; remote sign-out clears replicas on next launch
Content leakage through presencePresence only within the room; display name and colour, no email by default
[Threat][Control]

9.3 Observability

SignalMetric or logAlert threshold
Remote latencyProbe edit to receipt p95Above 200 ms for 10 min
Pending updatesClient-reported oldest unacked updatep99 above 60 s while online
ConvergenceChecksums of document state after sync (sampled)Any mismatch
Room loadRooms and connections per nodeAbove 80% of limit
Compaction backlogDocuments above log thresholdAbove 10 000 for 30 min

9.4 Cost estimate

ItemUnit costQuantityMonthly cost
Sync nodes[$/node-month]Example: 60[$]
Update log store[$/TB-month]Example: a few TB after compaction[$]
Snapshot object storage[$/TB-month]Example: 5 TB plus history[$]
Compaction workers[$/node-month][n][$]
Total[$]

10. Trade-offs and alternatives

DecisionOptionsChosenWhyWhat we give up
Merge approachLock paragraphs; last-write-wins; OT; CRDTCRDTOffline editing, no ordering server, proven librariesMetadata overhead, tombstones
Server roleAuthoritative OT server; relay + persistence; pure peer-to-peerRelay + persistenceAccess control, backup, simple scalingServer still needed for sharing
LibraryBuild own CRDT; adopt mature libraryAdoptCorrectness is hard; libraries are fuzz-tested with editor bindingsDependence on library's roadmap and format
PersistenceStore full document on every change; update log + snapshotsLog + snapshotsCheap appends, history, fast loadCompaction jobs to run and verify
TransportHTTP polling; WebSocket; WebRTC data channelsWebSocketWorks through proxies, simple server fan-outServer bandwidth for all relays
[Decision][Options][Chosen][Why][Cost]

11. Delivery plan and risks

11.1 Milestones

MilestoneScopeExit criterionTarget date
M1Local CRDT document with offline persistence, single userNo data loss across 10 000 fuzzed sessionsYYYY-MM-DD
M2Sync server, real-time collaboration, presenceConvergence checks clean in beta for 30 daysYYYY-MM-DD
M3Compaction, version historySnapshot verification 100% for 30 daysYYYY-MM-DD
M4Migration of existing documentsAll documents converted; old save path removedYYYY-MM-DD

11.2 Rollout and rollback

[Example: Convert documents lazily on first open in the new editor, keeping the old representation for 90 days. Rollback exports the CRDT state to the old format, which is lossless for content though not for fine-grained history.]

11.3 Risks

IDRiskLikelihoodImpactMitigationOwner
R-01Example: Document metadata grows too large for long-lived documentsMediumMediumRun-length encoding, sub-documents, size monitoring[@name]
R-02Example: Library format change breaks stored updatesLowHighPin versions; store format version; migration tests[@name]
R-03Example: Users surprised that revoked users keep offline copiesMediumMediumClear sharing UI; documentation[@name]
[R-nn][Risk][L / M / H][L / M / H][Mitigation][@name]

12. Appendices

12.1 Open questions

IDQuestionOwnerDue
OQ-01Example: Do we expose "suggesting" mode in v1?[@name]YYYY-MM-DD
OQ-02Example: Which library for the native mobile apps?[@name]YYYY-MM-DD
[OQ-nn][Question][@name]YYYY-MM-DD

12.2 Glossary

TermMeaning
OTOperational transformation: adjusting concurrent operations so they can be applied in any order
CRDTConflict-free replicated data type: data structure whose replicas merge deterministically
State vectorPer-client count of operations a replica has seen
TombstoneMarker for deleted content kept so later merges stay correct
AwarenessEphemeral presence state such as cursors and user names
Local-firstSoftware where the primary copy of data lives on the user's device
[Term][Meaning]

12.3 References

ReferenceUse
Shapiro et al., "Conflict-free Replicated Data Types" (2011)CRDT foundations
Nicolaescu et al., "Near Real-Time Peer-to-Peer Shared Editing on Extensible Data Types" (YATA, 2016)Sequence CRDT in 7.3
Weidner and Kleppmann, "The Art of the Fugue" (2023)Non-interleaving sequence CRDT
Kleppmann et al., "Local-first software" (Ink and Switch, 2019)Local-first principles in 7.7
RFC 6455 The WebSocket ProtocolTransport

Standard and licence note

Based on common industry practice and published research on collaborative editing cited in 12.3. This is a reference design to adapt, with illustrative numbers. Original text written for this collection. Template licence: MIT.

Template liên quan