Lesson 407 min read

Design a Collaborative Editor (Google Docs)

April 19, 2026

// TL;DR

Design a collaborative editor around a sync service that each editor joins over a WebSocket 'room'. Represent edits as small operations (insert/delete at a position) appended to a per-document op log — the source of truth. Resolve concurrent edits with Operational Transform (server rewrites conflicting ops, Google Docs' approach) or CRDTs (operations merge conflict-free, better offline/P2P). Broadcast each op to all editors for sub-100ms updates; snapshot periodically to bound replay; shard by document id to scale.

Design a Collaborative Editor (Google Docs)

“Design Google Docs” is the canonical real-time collaboration question. It looks like a document store, but the moment two people type in the same paragraph at the same moment, it becomes a distributed-systems problem: how do independent edits, applied in different orders on different machines, converge to the same document? The answer — Operational Transform or CRDTs — plus a low-latency sync layer, is what interviewers are really probing. Let’s build it.

Understanding the Problem

Functional Requirements

  1. Edit a shared document — Multiple users edit the same document simultaneously.
  2. Real-time sync — One user’s keystroke appears on everyone else’s screen within a fraction of a second.
  3. Conflict resolution — Concurrent edits to the same region merge without corrupting the document or losing work.
  4. Presence — Show who is in the document and where their cursor/selection is.
  5. Persistence & history — The document is saved durably and past versions can be restored.
  6. Offline editing — A user can keep editing offline; changes sync and merge on reconnect.

Non-Functional Requirements

Requirement Target
Edit propagation (p99) < 100ms between collaborators in a region
Concurrent editors/doc Up to ~100 active editors on one document
Consistency Strong convergence — all clients end at the identical document
Durability No committed edit is ever lost
Availability Edits continue through transient disconnects (queue & resync)
Scale Hundreds of millions of documents

Scale Estimation

  • The system scales by number of documents, not a single giant one — hundreds of millions of docs, but each with a small, bounded set of concurrent editors (usually 1–10, occasionally ~100).
  • An active editor emits maybe 1–5 small ops/sec while typing. A busy document is ~100 editors × a few ops/sec = a few hundred ops/sec — tiny per document, but multiplied across millions of live documents.

This shapes the architecture: each document is an independent, shardable unit with its own small op stream. There is no global state to coordinate — the hard problem is local to one document.

Core Entities and APIs

The key insight: don’t sync the whole document on every change. Sync tiny operations.

Data Model

Operation = {
    doc_id:   string
    op_id:    uuid
    author:   user_id
    type:     "insert" | "delete" | "format"
    pos:      int            # position (OT) or element id (CRDT)
    content:  string         # for inserts
    rev:      int64          # revision this op was based on
}

Document = {
    doc_id:   string
    rev:      int64          # current revision number
    snapshot: blob           # materialized text at some revision
    # full state = snapshot + ops since that snapshot
}

Presence = {
    doc_id:   string
    user_id:  string
    cursor:   int            # caret position / selection range
    color:    string
}       # ephemeral, not persisted

API Design

# Join a document (upgrades to a WebSocket)
GET /docs/{doc_id}/connect        → 101 Switching Protocols (WebSocket)
  on connect: server sends { snapshot, rev }

# Over the WebSocket (bidirectional messages)
→  { type: "op", op: {...}, base_rev }      # client sends an edit
←  { type: "op", op: {...}, rev }           # server broadcasts others' edits
←  { type: "ack", op_id, rev }              # server confirms + assigns revision
↔  { type: "presence", cursor }             # cursor / selection updates

# Snapshot / history (plain HTTP)
GET /docs/{doc_id}/history
POST /docs/{doc_id}/revert  { to_rev }

Representing edits as position-based ops (insert "x" at 42) rather than “here’s the new document” is what makes sub-100ms sync and conflict resolution possible.

High-Level Design

Collaborative editor architecture: clients connect over WebSocket to a sync service per document room, which appends edits to an op log, writes periodic snapshots to a document store, broadcasts ops to peers, and tracks presence
  • Clients hold the current document plus a local op buffer. They apply their own edits immediately (optimistic UI) and send ops to the server.
  • Sync service — Each document is a “room”. All editors of a document connect (via WebSocket) to the same room, handled by one authoritative process. The service orders incoming ops, transforms/merges them, assigns a revision number, and broadcasts them to every other editor in the room.
  • Op log — An append-only, ordered log of operations per document is the source of truth. The document at any revision = a snapshot plus the ops after it.
  • Document store — Periodic snapshots (the materialized text at a revision) are persisted so you don’t replay millions of ops to open a document.
  • Presence service — Ephemeral cursor/selection state, broadcast to the room but not persisted.

A routing layer maps each doc_id to the server currently hosting its room (via a consistent-hash ring or a coordination service), so all of a document’s editors land on the same authoritative node.

Deep Dive: Conflict Resolution (OT vs CRDT)

This is the crux of the problem. Two users start from revision 5. Alice inserts “X” at position 2; Bob deletes the character at position 1 — at the same instant. Applied naively in different orders, their documents diverge. The fix is a merge algorithm that guarantees convergence: everyone ends identical regardless of order.

Two approaches to merging concurrent edits: Operational Transform rewrites one operation against a concurrent one so order no longer matters; CRDTs make operations commute so they merge conflict-free in any order

Operational Transform (OT)

OT transforms an operation against concurrent operations so it still has the intended effect. If Bob deleted position 1 before Alice’s “insert at 2” is applied, Alice’s op is transformed to “insert at 1” — its position is shifted to account for Bob’s edit. A central server imposes a global order and transforms each incoming op against the ops it hasn’t yet seen.

  • This is Google Docs’ approach. It is efficient (ops stay small, no per-character metadata) and battle-tested.
  • The cost: the transform functions are notoriously tricky to get correct for every op-pair, and OT essentially requires a central server to establish order — it doesn’t merge well peer-to-peer.
# Transform: adjust incoming op against an already-applied concurrent op
def transform(incoming, applied):
    if incoming.type == "insert" and applied.type == "insert":
        if applied.pos <= incoming.pos:
            incoming.pos += len(applied.content)   # shift right
    # ... delete/insert, insert/delete, delete/delete cases ...
    return incoming

CRDTs (Conflict-free Replicated Data Types)

CRDTs design the operations so they commute — applying them in any order yields the same result — so no transform step is needed. A sequence CRDT (RGA, LSEQ, Yjs/Automerge) gives every inserted character a unique, immutable identifier with a fractional/logical position, so “insert after id Z” is unambiguous no matter what else happened.

  • CRDTs merge without a central coordinator, which makes them excellent for offline and peer-to-peer editing — exactly the hard cases.
  • The cost: each element carries metadata (ids, tombstones for deletes), so memory and payloads are larger, and garbage-collecting tombstones adds complexity.

Which to choose

State the trade-off explicitly: OT when you have a reliable central server and want minimal per-op overhead (classic Docs); CRDTs when offline-first or decentralized merging matters (Figma, modern local-first apps lean CRDT). Either satisfies the real requirement — strong eventual consistency: once all ops are delivered, every replica is identical.

Deep Dive: The Sync Layer and Offline

WebSocket rooms

Editing needs two-way, low-latency push, so the sync service uses WebSockets, not polling. All editors of a document share one room on one authoritative server. When a client sends an op, the server:

  1. Transforms/merges it against any concurrent ops (OT) or integrates it (CRDT).
  2. Appends it to the op log and assigns the next revision number.
  3. Acks the author and broadcasts the op to every other editor in the room.

Clients apply their own edits optimistically (instant local feedback) and reconcile when the server’s authoritative version arrives — so typing never feels laggy even over a slow link.

Offline and reconnect

Each client tracks the last server revision it has seen. While offline it queues local ops. On reconnect it sends its queued ops with their base revision; the server transforms them against everything that happened meanwhile and integrates them, then streams back the ops the client missed. Because the merge algorithm guarantees convergence, offline edits fold in cleanly — this is precisely where CRDTs shine and where OT needs careful base-revision bookkeeping.

Snapshots bound the cost

Replaying millions of ops to open an old document is infeasible, so the system writes a snapshot (materialized text at revision R) periodically. Opening a document loads the latest snapshot plus the handful of ops after it. Snapshots also power version history and “revert to” — each is a named point you can reconstruct.

Deep Dive: Persistence, Presence and Scaling

  • Durability — The op log is written durably (and replicated) before an op is acked, so no confirmed edit is lost even if the room’s server crashes. On crash, another node loads the latest snapshot + log and resumes the room.
  • Presence — Cursors and selections change constantly but don’t need persistence or the same ordering guarantees as edits. Broadcast them on a separate, best-effort channel (and throttle to ~10/sec) so presence traffic never competes with edits.
  • Sharding by document — Because documents are independent, scale horizontally by assigning documents to servers (consistent hashing on doc_id). A single document’s throughput is small and bounded, so one server comfortably hosts many rooms; only a pathologically popular document (thousands of editors) needs special handling — typically by making most participants read-only viewers served from a snapshot + broadcast, with a capped set of active editors.
  • Access control — Check permissions on connect (owner/editor/viewer); viewers receive broadcasts but their ops are rejected.

Key Takeaways

Decision Choice Why
Unit of sync Small operations, not whole documents Enables sub-100ms updates and principled merging
Conflict resolution Operational Transform or CRDT Guarantees all replicas converge regardless of order
OT vs CRDT OT = central, compact (Docs); CRDT = offline/P2P Match to whether you have a reliable server
Transport WebSocket rooms, one authoritative node/doc Two-way low-latency push; a single ordering point
Source of truth Append-only op log + periodic snapshots Durability, history, and fast document open
Scale Shard by doc_id (consistent hashing) Documents are independent; no global coordination
Presence Separate best-effort channel Cursor spam never blocks real edits

A collaborative editor is a consistency problem in disguise. The interviewer wants to hear the words “Operational Transform” or “CRDT” and, more importantly, why they guarantee convergence; then a WebSocket sync layer with an op log, optimistic local application, snapshots, and sharding by document. Draw two users editing the same line and walk through how the merge keeps them identical — that is the moment the interview is won.