Google Docs / Figma collaborative editor system design case. WebSocket gateway + per-doc session actor with CRDT merge, sharded op log (Spanner), snapshot service to blob storage, Redis presence, ACL, comments, version history, Kafka fan-out for hot docs. 5 scenarios: keystroke, concurrent edit at same anchor (CRDT merge), offline edit + sync, presence cursors, hot doc with snapshot + version restore. 2 ADRs: OT vs CRDT (pick CRDT), server-authoritative vs P2P CRDT (pick server-authoritative).
Collaborative editor кажется фронтендовой задачей, пока два пользователя не начнут редактировать один символ в офлайне, а третий не восстановит вчерашнюю версию документа. В реальности это distributed system с жестким latency budget, конфликтами, offline sync, durable op log, presence, ACL, comments и version history. Главный критерий качества: все участники должны сходиться к одному состоянию без ручного разрешения большинства конфликтов.
Целевой масштаб Google Docs/Figma class: до 10B documents, 2B MAU, десятки миллионов concurrent editors, десятки миллионов active docs. Active doc может получать 5-30 edit ops/sec, а общий поток может достигать сотен миллионов ops/sec. Средняя op маленькая, около 100 bytes, но fan-out умножает ее на collaborators. Latency edit-to-peer должна быть меньше 100ms p95, open doc меньше 500ms p95.
Кейс тренирует ::concept{slug="crdts"}, ::concept{slug="websocket"}, ::concept{slug="realtime-collab-architecture"} и ::concept{slug="offline-first"}. Он показывает, почему realtime UX строится не только на WebSocket, а на корректной модели операций, снапшотах, логах, ACL и recovery.
Думайте о документе как о последовательности операций, а не как о файле, который постоянно перезаписывается. Пользователь A отправляет insert("h", pos=42), пользователь B одновременно отправляет delete(pos=42), пользователь C находится офлайн и копит свои операции в IndexedDB. Система должна принять все операции, упорядочить или слить их и сделать так, чтобы все реплики получили одинаковый итог.
Второй mental model: collaboration plane и persistence plane разные. Collaboration plane держит WebSocket connections, per-doc session actor, ephemeral presence and broadcast. Persistence plane хранит append-only op log, snapshots, comments, ACL and version history. Presence курсора можно потерять без катастрофы. Op log потерять нельзя.
Третий mental model: CRDT или OT это не замена серверу. CRDT дает математическое слияние concurrent operations и offline-first свойства, но production системе все равно нужен trusted server для ACL, durable storage, quota, abuse control, version history и fan-out. Поэтому практичный дизайн часто server-authoritative CRDT: клиенты имеют локальную реплику, а per-doc actor является canonical broadcaster.
Диаграмма показывает Clients, Realtime Plane, Persistence и Hot-doc fan-out. Editors A/B/C подключаются через WSS к Edge WS Gateway. Gateway проверяет ACL/Sharing на connect и sticky-route-ит соединение к Doc Session actor по doc_id. Session держит in-memory state документа или recent tail, принимает ops, применяет CRDT merge, пишет durable op log и рассылает изменения peers.
Presence хранится в Redis с TTL, например 30 секунд. Cursor moves и selection updates не пишутся в op log, иначе они увеличат write rate на порядки и загрязнят version history. Comments, suggestions and version history идут в отдельные сервисы, но привязаны к document version или CRDT anchors.
Persistence состоит из Op Log, Snapshot Service и Blob Store. Op Log sharded by doc_id хранит (doc_id, seq, author_id, vclock, op, ts). Snapshot Service каждые N ops или 5 минут строит сжатый snapshot, сохраняет его в blob storage и оставляет tail ops для fast open. Для hot docs Session может публиковать op в Kafka topic, а fan-out workers рассылают обновление тысячам viewers, чтобы actor не тратил весь CPU на network writes.
Keystroke scenario показывает обычную печать. Editor A локально применяет op для instant feedback, отправляет ее через WebSocket, session actor делает CRDT merge, append в op log, затем broadcast A/B/C и ack author-у с global seq. Урок: UX мгновенный локально, но durable acceptance приходит позже. Client должен уметь показывать pending ops и корректно обрабатывать server ack.
Concurrent edit scenario показывает A и B, которые вставляют символ в одну позицию. OT решает это через transform functions и центральный порядок операций. CRDT решает через stable identifiers, replica_id/vector clock и deterministic ordering. Урок: оба подхода возможны, но для offline-first в 2026 разумно выбирать CRDT, принимая metadata overhead and tombstone GC.
Offline scenario показывает laptop sleep или metro tunnel. Client продолжает редактировать локальную CRDT replica, копит 50 ops в IndexedDB, затем reconnect-ится с last_seen_seq и pending batch. Session проверяет ACL заново, принимает ops, отдает missed ops и делает rebroadcast peers. Урок: offline sync должен быть штатным flow, а не “пользователь потерял изменения”.
Presence scenario показывает курсор 30Hz. События идут в Redis TTL and debounced broadcast, но не persist-ятся. Если client закрывает laptop без disconnect, TTL сам убирает stale cursor. Урок: ephemeral data должна иметь другую надежность и стоимость, чем document ops.
Snapshot/version scenario показывает 1000 ops since last snapshot. Snapshot service читает base snapshot + tail, применяет ops, сжимает состояние и пишет v18. New collaborator открывает doc: получает snapshot v18 и tail ops. Version restore не удаляет историю, а создает новую операцию, которая переводит документ к выбранному состоянию; это audit-friendly and undoable.
Первый trade-off: OT против CRDT. OT экономнее по metadata и исторически использовался в Google Docs/EtherPad, но transform matrix сложна и плохо сочетается с длительным offline. CRDT хранит больше metadata, tombstones and vector clocks, зато merge commutative and deterministic. Для нового offline-first редактора CRDT обычно прагматичнее, если есть snapshot compaction and tombstone GC.
Второй trade-off: server-authoritative sessions против pure P2P CRDT. P2P красиво работает в прототипах и снижает server cost, но ACL, persistence, NAT traversal, version history and abuse control все равно требуют infrastructure. Server-authoritative actor добавляет central dependency, зато дает trusted ordering, audit and scalable fan-out.
Третий trade-off: per-doc actor против stateless processing. Actor упрощает ordering, in-memory cache и fan-out для одного doc, но hot document может стать hotspot. Stateless workers проще масштабируются, но сложнее поддерживать consistent per-doc state. Для hot docs нужен sharding/fan-out: actor принимает and persists ops, Kafka/fanout workers рассылают viewers.
Четвертый trade-off: snapshot frequency. Частые snapshots ускоряют open doc и уменьшают replay tail, но увеличивают storage/write cost. Редкие snapshots экономят, но новый collaborator будет долго replay-ить ops. Практичный выбор: every 1000 ops или 5 минут, плюс adaptive policy для hot docs.
Пятый trade-off: exact presence против lossy presence. Cursor positions должны быть smooth, но они не обязаны быть durable. Throttling/debounce снижает traffic, иногда курсор будет slightly stale. Это приемлемо, если document content remains correct.
Google Docs исторически связывают с Operational Transform и centralized collaboration server. Figma популяризировала CRDT-like multiplayer model with server coordination and efficient rendering. Notion, Linear, JupyterLab, Yjs and Automerge ecosystem показывают современный сдвиг к CRDT для offline and local-first experiences.
В production редакторах есть много смежных систем: permissions, sharing links, comments, suggestions, notifications, search indexing, export, import, malware scanning, quotas, enterprise audit logs and legal hold. Но ни одна из них не должна ломать core invariant: accepted document operations are durable and converge.
Для больших документов важны performance details: chunked text/tree representation, lazy loading sections, binary encoding ops, compression, garbage collection tombstones, mobile memory limits and rendering scheduler. System design диаграмма не обязана показывать все frontend internals, но backend protocol должен учитывать их.
Главная ошибка: хранить весь документ как last-write-wins blob. Два пользователя сохранят разные версии, и последний overwrite потеряет чужие изменения. Вторая ошибка: писать presence/cursor events в op log. Это раздувает историю и ломает replay cost.
Третья ошибка: не проверять ACL на reconnect and session attach. Пользователь мог потерять доступ, пока был офлайн. Pending local ops нельзя принимать без новой авторизации. Четвертая: не иметь durable local queue на клиенте. Если вкладка refresh-нулась до ack, пользователь потеряет текст.
Пятая ошибка: не ограничивать offline window and tombstone GC. Если GC удалил metadata, нужную для merge старой offline replica, sync должен требовать full reload или manual conflict handling. Шестая: делать broadcast before durable append без recovery protocol. Peers увидят op, которая исчезнет после crash.
Седьмая ошибка: не версионировать protocol. Старые mobile clients будут отправлять ops старого формата, и server должен уметь reject/upgrade gracefully. Восьмая: считать WebSocket надежным. Connections will drop; protocol needs ack, seq, retry, backpressure and resume.
Не нужен CRDT collaboration stack для простого single-user notes app без offline merge. Там достаточно optimistic save, version number and conflict dialog. Не стоит строить Google Docs архитектуру для формы, где одновременно редактирует один пользователь; row-level optimistic locking дешевле.
OT может быть уместен, если продукт всегда online, есть central server, строгая экономия metadata и команда уже имеет проверенную библиотеку/protocol. CRDT не является бесплатным: он увеличивает state size, требует compaction and careful encoding.
Pure P2P local-first подход может быть хорош для privacy-first desktop app, где нет enterprise ACL, comments and centralized audit. Но для multi-tenant SaaS с sharing links and compliance server-authoritative model обычно проще защитить.
::concept{slug="crdts"} для sequence CRDT, tombstones, vector clocks and convergence.::concept{slug="websocket"} для persistent connections, backpressure, reconnect and heartbeats.::concept{slug="realtime-collab-architecture"} для session actors, fan-out, presence and persistence.::concept{slug="offline-first"} для local queue, IndexedDB, sync and conflict handling.