Skip to content

Real-Time Collaboration System Design — Google Docs Architecture Guide

DodaTech Updated 2026-06-24 6 min read

In this tutorial, you'll learn about Real. We cover key concepts, practical examples, and best practices.

Real-time collaboration systems enable multiple users to edit shared state simultaneously with instant synchronization, using Operational Transformation (OT) or CRDT-based conflict resolution to merge concurrent edits without data loss.

Why Real-Time Collaboration Design Matters

Collaborative editing is one of the hardest problems in distributed systems. Multiple users insert and delete characters at the same position simultaneously. Without a correct algorithm, edits are lost, cursors jump, and documents corrupt. Google Docs supports millions of concurrent editors. Figma enables real-time design collaboration with 500+ users on a single file. Notion, Slab, and Coda all built collaborative editors. Doda Browser's shared bookmark lists needed real-time collaboration — using CRDTs, two users can simultaneously add, remove, and reorder bookmarks without conflicts.

Plain-Language Explanation

Imagine two people writing on the same whiteboard with different markers. If Alice writes "A" where Bob is writing "B", they collide. You need rules: either one person goes first (OT) or you design the markers so the letters merge automatically (CRDT). OT is like having a referee who decides the order. CRDT is like giving each person their own section of the whiteboard — the sections don't overlap, so there's no conflict.

graph LR
    subgraph "Clients"
        C1[User A]
        C2[User B]
        C3[User C]
    end
    C1 --> WS1[WebSocket]
    C2 --> WS2[WebSocket]
    C3 --> WS3[WebSocket]
    WS1 --> SERVER[Collaboration Server]
    WS2 --> SERVER
    WS3 --> SERVER
    SERVER --> OT[Operational Transformation
Engine] OT --> DOC[(Document State
In-Memory)] DOC --> SNAPSHOT[Periodic Snapshot
Database] OT --> VER[Version Vector
/ Causality] SERVER --> BROADCAST[Broadcast
to Other Clients] style SERVER fill:#e67e22,color:#fff style OT fill:#3498db,color:#fff style DOC fill:#27ae60,color:#fff

WebSocket Connection Setup

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Set
import json

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active: dict[str, Set[WebSocket]] = {}

    async def connect(self, doc_id: str, ws: WebSocket):
        await ws.accept()
        if doc_id not in self.active:
            self.active[doc_id] = set()
        self.active[doc_id].add(ws)

    def disconnect(self, doc_id: str, ws: WebSocket):
        self.active[doc_id].discard(ws)

    async def broadcast(self, doc_id: str, message: dict, sender: WebSocket = None):
        for ws in self.active.get(doc_id, set()):
            if ws != sender:
                await ws.send_json(message)

manager = ConnectionManager()

@app.websocket("/ws/{doc_id}")
async def websocket_endpoint(ws: WebSocket, doc_id: str):
    await manager.connect(doc_id, ws)
    try:
        while True:
            data = await ws.receive_json()
            await manager.broadcast(doc_id, data, sender=ws)
    except WebSocketDisconnect:
        manager.disconnect(doc_id, ws)

Operational Transformation

Transform an insert operation against a concurrent delete:

class OpInsert:
    def __init__(self, pos: int, char: str):
        self.pos = pos
        self.char = char

    def transform_against(self, other) -> 'OpInsert':
        if isinstance(other, OpInsert):
            if other.pos < self.pos or (other.pos == self.pos and other.char < self.char):
                return OpInsert(self.pos + 1, self.char)
        elif isinstance(other, OpDelete):
            if other.pos < self.pos:
                return OpInsert(self.pos - 1, self.char)
            elif other.pos == self.pos:
                return OpInsert(self.pos, self.char)
        return OpInsert(self.pos, self.char)

    def apply(self, doc: list) -> list:
        doc.insert(self.pos, self.char)
        return doc

class OpDelete:
    def __init__(self, pos: int):
        self.pos = pos

    def transform_against(self, other):
        if isinstance(other, OpInsert):
            if other.pos <= self.pos:
                return OpDelete(self.pos + 1)
        elif isinstance(other, OpDelete):
            if other.pos < self.pos:
                return OpDelete(self.pos - 1)
            elif other.pos == self.pos:
                return None
        return OpDelete(self.pos)

    def apply(self, doc: list) -> list:
        if 0 <= self.pos < len(doc):
            doc.pop(self.pos)
        return doc

CRDT-Based Character Store

A CRDT approach using unique character IDs:

import uuid

class CRDTChar:
    def __init__(self, char: str, site_id: str, position: tuple):
        self.id = (site_id, uuid.uuid4().hex)
        self.char = char
        self.position = position

class CRDTDocument:
    def __init__(self, site_id: str):
        self.site_id = site_id
        self.chars: list[CRDTChar] = []
        self.clock = 0

    def insert(self, pos: int, char: str):
        self.clock += 1
        if pos == 0:
            position = (0, self.site_id, self.clock)
        elif pos >= len(self.chars):
            position = (len(self.chars), self.site_id, self.clock)
        else:
            position = self.chars[pos - 1].position + (1,)
        new_char = CRDTChar(char, self.site_id, position)
        self.chars.insert(pos, new_char)
        return new_char

    def delete(self, pos: int):
        if 0 <= pos < len(self.chars):
            char = self.chars.pop(pos)
            return char
        return None

    def get_text(self) -> str:
        return "".join(c.char for c in self.chars)

doc = CRDTDocument("site-a")
doc.insert(0, "H")
doc.insert(1, "i")
doc.insert(2, "!")
print(f"Text: {doc.get_text()}")

Expected output:

Text: Hi!

Server-Side Document State

Maintain document state with version vectors:

class DocumentState:
    def __init__(self, doc_id: str):
        self.doc_id = doc_id
        self.text = ""
        self.version_vectors: dict[str, int] = {}

    def apply_operation(self, site_id: str, op: dict) -> bool:
        self.version_vectors[site_id] = self.version_vectors.get(site_id, 0) + 1
        if op["type"] == "insert":
            self.text = self.text[:op["pos"]] + op["char"] + self.text[op["pos"]:]
        elif op["type"] == "delete":
            if op["pos"] < len(self.text):
                self.text = self.text[:op["pos"]] + self.text[op["pos"] + 1:]
        return True

    def to_snapshot(self) -> dict:
        return {"doc_id": self.doc_id, "text": self.text, "version_vectors": self.version_vectors}

Common Mistakes

  1. No conflict resolution strategy: Without OT or CRDT, concurrent edits cause data loss. Always implement a mathematically proven algorithm.

  2. Server as single point of failure: If the sync server goes down, all collaboration stops. Use WebSocket reconnection with state reconciliation.

  3. Cursor synchronization lag: Cursor positions of other users arriving late causes jarring jumps. Send cursor updates at a throttled rate (every 50-100ms).

  4. No undo support: Undo requires inverse operations and must be handled correctly across users. Track operation history per user.

  5. Scaling WebSocket connections: A single server handles ~10K concurrent WebSocket connections. Beyond that, use a distributed pub/sub (Redis Pub/Sub, Kafka) to fan out operations across servers.

Practice Questions

  1. OT vs CRDT: which is better? OT is simpler for linear text but requires a central server. CRDTs work peer-to-peer, handle arbitrary merges without a server, and are used by Figma and Atom Teletype. OT is used by Google Docs.

  2. How does Google Docs resolve conflicts? Google Docs uses OT with a central server. The server maintains document state, transforms incoming operations against concurrent ops, and broadcasts transformed ops.

  3. What is a version vector? A mapping of site_id to operation count, used to track which operations each site has seen. Two sites have converged when their version vectors are identical.

  4. How do you handle offline edits? Buffer operations locally and sync when reconnected. CRDTs handle this naturally — operations can be applied in any order and produce the same result.

  5. What's the difference between WebSocket and SSE for real-time collaboration? WebSocket is bidirectional (client sends edits, receives edits). SSE is server-to-client only. Collaboration needs bidirectional, so WebSocket is mandatory.

Mini Project

Build a collaborative text editor sync core:

import asyncio, json
from fastapi import FastAPI, WebSocket
from collections import defaultdict

app = FastAPI()
doc_text = ""
doc_ops: list[dict] = []

class OTSync:
    @staticmethod
    def transform(op1: dict, op2: dict) -> dict:
        if op1["type"] == "insert" and op2["type"] == "insert":
            if op2["pos"] <= op1["pos"] or (op2["pos"] == op1["pos"] and op2.get("site", "") < op1.get("site", "")):
                return {**op1, "pos": op1["pos"] + 1}
        elif op1["type"] == "insert" and op2["type"] == "delete":
            if op2["pos"] < op1["pos"]:
                return {**op1, "pos": op1["pos"] - 1}
        elif op1["type"] == "delete" and op2["type"] == "insert":
            if op2["pos"] <= op1["pos"]:
                return {**op1, "pos": op1["pos"] + 1}
        return op1

connections: dict[str, set] = defaultdict(set)

@app.websocket("/ws/{doc_id}")
async def doc_ws(ws: WebSocket, doc_id: str):
    await ws.accept()
    await ws.send_json({"type": "snapshot", "text": doc_text, "ops": doc_ops[-50:]})
    connections[doc_id].add(ws)
    try:
        while True:
            data = await ws.receive_json()
            doc_ops.append(data)
            if data["type"] == "insert":
                global doc_text
                doc_text = doc_text[:data["pos"]] + data["char"] + doc_text[data["pos"]:]
            elif data["type"] == "delete":
                doc_text = doc_text[:data["pos"]] + doc_text[data["pos"] + 1:]
            for c in connections[doc_id]:
                if c != ws:
                    await c.send_json(data)
    except:
        connections[doc_id].discard(ws)

Cross-References

  • Distributed Queue
  • Event Sourcing
  • Distributed Counter
  • Consistent Hashing
  • WebSocket

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro