0

System Design: Thiết kế Collaborate Document Editing tools như Google docs

image.png

1. Mục tiêu hệ thống

Thiết kế một hệ thống chỉnh sửa tài liệu cộng tác theo thời gian thực giống Google Docs, nơi nhiều người dùng có thể cùng mở một tài liệu, cùng chỉnh sửa, nhìn thấy thay đổi của nhau gần như tức thì, nhìn thấy cursor/presence của nhau, và hệ thống vẫn có thể lưu lịch sử thay đổi để phục hồi hoặc undo/redo.

Mục tiêu chính không phải chỉ là “lưu file”, mà là xử lý real-time collaboration:

  • Nhiều user cùng sửa một document.
  • Thay đổi phải được truyền đến các user khác gần như ngay lập tức.
  • Nếu hai user sửa cùng lúc, hệ thống phải xử lý conflict.
  • Document phải được lưu bền vững.
  • Cursor, presence, typing indicator là dữ liệu tạm thời, không nên ghi liên tục vào database chính.
  • Hệ thống phải scale được đến hàng triệu kết nối WebSocket.

2. Requirements

2.1 Functional Requirements

Hệ thống cần hỗ trợ:

  1. User tạo document mới.
  2. User mở document.
  3. User chỉnh sửa document theo thời gian thực.
  4. Nhiều user cùng xem và cùng sửa một document.
  5. User nhìn thấy thay đổi của những collaborator khác.
  6. User nhìn thấy cursor / selection / presence của người khác.
  7. Hệ thống lưu lịch sử chỉnh sửa.
  8. Hệ thống hỗ trợ quyền truy cập như owner, editor, viewer.
  9. Hệ thống có thể load document nhanh khi user mở lại.
  10. Hệ thống có thể phục hồi document từ snapshot + operation log.

2.2 Non-functional Requirements

Latency

Với collaborative editing, latency nên được nói bằng con số cụ thể.

Mục tiêu hợp lý:

Propagation latency < 100ms

Nghĩa là sau khi User A gõ một thay đổi, User B nên nhìn thấy thay đổi đó trong dưới 100ms, vì nếu lâu hơn, người dùng sẽ bắt đầu cảm thấy hệ thống không còn “real-time”.

Availability

Google Docs-like system nên ưu tiên availability hơn strict consistency.

Lý do:

  • Nếu chờ strict consistency trên tất cả node thì có thể phải lock document.
  • Lock sẽ làm trải nghiệm cộng tác real-time rất tệ.
  • User có thể chấp nhận nhìn thấy version hơi cũ trong thời gian rất ngắn.
  • Nhưng user không muốn bị block không cho gõ.

Vì vậy:

Availability > Strict Consistency

Consistency Model

Document state có thể dùng eventual consistency, nhưng operation ordering trong cùng một document cần được kiểm soát.

Ta không cần tất cả user luôn nhìn thấy cùng một state tại cùng một microsecond, nhưng cuối cùng tất cả client phải converge về cùng một document state.

Có hai hướng phổ biến:

  • Operational Transformation (OT)
  • CRDT

Trong thiết kế này, ta chọn Operational Transformation (OT).


3. Core Entities

3.1 User

Đại diện cho người dùng trong hệ thống.

User {
  user_id
  name
  email
}

3.2 Document

Đại diện cho metadata của document.

Document {
  document_id
  title
  owner_id
  latest_version
  latest_snapshot_version
  created_at
  updated_at
}

Lưu ý: Document table không nhất thiết lưu toàn bộ content sau mỗi lần edit. Content có thể được build từ snapshot + operation log.

3.3 DocumentPermission

Quản lý quyền truy cập document.

DocumentPermission {
  document_id
  user_id
  role: owner | editor | viewer
}

3.4 Operation / Change / Edit

Đây là entity rất quan trọng.

Mỗi thay đổi trong document được lưu thành một operation.

Operation {
  document_id
  version
  operation_id
  user_id
  type: insert | delete | update
  position
  content
  base_version
  timestamp
}

Operation giúp hệ thống:

  • Lưu lịch sử thay đổi.
  • Replay để khôi phục document.
  • Apply OT để xử lý conflict.
  • Hỗ trợ undo/redo.
  • Audit/debug khi có lỗi.

3.5 Cursor / Presence

Cursor là dữ liệu real-time tạm thời.

Cursor {
  document_id
  user_id
  cursor_position
  selection_range
  connection_status
  updated_at
}

Cursor không nên ghi liên tục vào database chính. Nó nên được lưu trong Redis với TTL.

Ví dụ Redis record:

presence:{document_id}:{user_id} = {
  user_id,
  document_id,
  cursor_position,
  connection_status,
  updated_at
}
TTL = 30s

Khi WebSocket mở, user được mark online. Khi WebSocket đóng hoặc timeout, user được mark offline.


4. API Design

4.1 REST APIs

REST API dùng cho các thao tác request-response như tạo document, lấy metadata, quản lý quyền.

Không nên viết endpoint có verb dư thừa như:

POST /docs/create

Vì HTTP method POST đã nói hành động create. Nên viết:

POST /docs

Một số API chính:

POST /docs
GET /docs/{docId}
GET /docs/{docId}/metadata
GET /docs/{docId}/permissions
POST /docs/{docId}/permissions
DELETE /docs/{docId}/permissions/{userId}
GET /docs/{docId}/snapshots/latest
GET /docs/{docId}/operations?afterVersion={version}

4.2 Authentication

User identity không nên truyền trong body như:

{
  "editor_id": "user_123"
}

Thay vào đó, identity nên lấy từ:

Authorization: Bearer <token>

Hoặc được xác thực trong WebSocket handshake.

Lý do:

  • Tránh spoof user_id.
  • Tránh lộ sensitive identifier trong logs.
  • Đúng chuẩn API security hơn.

4.3 WebSocket API

Real-time editing cần WebSocket vì REST chỉ là request-response, server không chủ động push update được.

Endpoint:

WS /docs/{docId}

WebSocket là kết nối hai chiều trên cùng một connection:

Client -> Server: gửi edit/cursor update
Server -> Client: push edit/cursor/presence update

Không cần polling liên tục.

Các message type tối thiểu:

{
  "type": "insert",
  "document_id": "doc_123",
  "base_version": 100,
  "position": 20,
  "content": "hello"
}
{
  "type": "delete",
  "document_id": "doc_123",
  "base_version": 101,
  "position": 20,
  "length": 5
}
{
  "type": "update",
  "document_id": "doc_123",
  "base_version": 102,
  "position": 20,
  "content": "new text"
}
{
  "type": "updateCursor",
  "document_id": "doc_123",
  "cursor_position": 35,
  "selection_range": [35, 40]
}

Cursor update là message type quan trọng. Nếu thiếu cursor update, user sẽ không nhìn thấy vị trí chỉnh sửa của người khác.


5. High-level Architecture

                    +------------------+
                    |      Client      |
                    | Web / Mobile App |
                    +--------+---------+
                             |
                             | HTTPS / WebSocket
                             v
                    +------------------+
                    | API Gateway / LB |
                    +--------+---------+
                             |
           +-----------------+-----------------+
           |                                   |
           v                                   v
+----------------------+           +----------------------+
|  Document Service A  |           |  Document Service B  |
|  WebSocket Server    |           |  WebSocket Server    |
+----------+-----------+           +----------+-----------+
           |                                  |
           | Pub/Sub                         | Pub/Sub
           v                                  v
        +------------------------------------------+
        |        Redis Pub/Sub / Kafka             |
        |        Cross-server fanout               |
        +------------------------------------------+
           |
           v
+--------------------------+
| Document Operations DB   |
| DynamoDB / Cassandra     |
+--------------------------+
           |
           v
+--------------------------+
| Snapshot Storage         |
| S3 / Blob Storage        |
+--------------------------+

+--------------------------+
| Redis                    |
| Cursor / Presence / TTL  |
+--------------------------+

Vai trò từng component

Client

Client giữ local document state, gửi operation lên server, nhận operation từ server và apply vào local state.

Client cũng cần xử lý incoming transformed operations để reconcile với những local edits chưa được server xác nhận.

API Gateway / Load Balancer

Chịu trách nhiệm:

  • TLS termination.
  • Authentication routing.
  • Rate limiting.
  • Forward HTTP/WebSocket connection đến Document Service.

Document Service

Đây là service quan trọng nhất.

Nó xử lý:

  • WebSocket connection.
  • Validate permission.
  • Apply OT.
  • Assign document version.
  • Persist operation.
  • Publish event ra pub/sub.
  • Push update về client.
  • Manage connection mapping tạm thời.

Redis

Dùng cho dữ liệu tạm thời:

  • cursor position
  • presence
  • typing indicator
  • user online/offline
  • document_id -> active connections
  • TTL records

Không dùng Redis làm source of truth cho document content.

Pub/Sub Layer

Dùng để broadcast event giữa nhiều Document Service instance.

Nếu User A ở Server A sửa document, nhưng User B đang connect ở Server B, thì Server A cần một cách để báo Server B push update đến User B.

Server A receives edit
Server A publishes event to doc_123 topic
Server B subscribed to doc_123 topic
Server B receives event
Server B pushes update to User B

Document Operations DB

Lưu operation log bền vững.

Có thể dùng:

  • DynamoDB
  • Cassandra

Phù hợp vì workload write-heavy.

Snapshot Storage

Lưu snapshot document theo version.

Có thể dùng:

  • S3
  • Blob storage
  • Distributed file storage
  • Hoặc DB nếu document nhỏ

6. Real-time Editing Flow

6.1 User mở document

1. Client gọi GET /docs/{docId}/metadata
2. Server validate permission
3. Client mở WebSocket: WS /docs/{docId}
4. Document Service xác thực user trong WebSocket handshake
5. Server load latest snapshot
6. Server replay operations sau snapshot
7. Client nhận document state mới nhất
8. Server mark user online trong Redis
9. Server broadcast presence update cho collaborator khác

6.2 User gửi edit

1. User gõ text trên client
2. Client tạo operation với base_version hiện tại
3. Client apply optimistic update vào local document
4. Client gửi operation qua WebSocket
5. Document Service nhận operation
6. Service kiểm tra permission
7. Service apply OT nếu base_version cũ hơn latest_version
8. Service assign version mới
9. Service ghi operation vào Document Operations DB
10. Service publish event ra Redis Pub/Sub / Kafka
11. Các Document Service liên quan nhận event
12. Server push transformed operation đến các client đang mở document
13. Client nhận operation và apply vào local state

6.3 Cursor update flow

Cursor update không cần ghi vào DB chính.

1. Client gửi updateCursor qua WebSocket
2. Document Service ghi cursor position vào Redis với TTL
3. Service publish cursor update qua pub/sub
4. Các server có user đang xem document nhận event
5. Server push cursor update đến client liên quan

Cursor update là dữ liệu tạm thời. Nếu mất một cursor event thì không ảnh hưởng đến document content.


7. Operational Transformation

7.1 Vì sao cần OT?

Nếu hai user cùng sửa document tại cùng một thời điểm, operation có thể conflict.

Ví dụ document ban đầu:

Hello World

User A insert "Beautiful " tại vị trí 6:

Hello Beautiful World

User B delete "World" tại vị trí 6 theo version cũ.

Nếu server apply trực tiếp mà không transform, vị trí edit có thể sai.

OT giúp transform operation dựa trên các operation đã xảy ra trước đó để tất cả client cuối cùng converge về cùng một state.

7.2 Server-side OT

Document Service nhận operation với base_version.

Nếu base_version < latest_version, server lấy các operation từ base_version + 1 đến latest_version và transform operation mới.

incoming_op = user edit at base_version 100
latest_version = 105

server transforms incoming_op against operations 101..105
server assigns version 106
server persists operation 106
server broadcasts operation 106

7.3 Client-side OT

OT không chỉ xảy ra ở server.

Client cũng có thể đã apply optimistic local changes nhưng chưa được server ack. Khi client nhận operation từ user khác, nó cần transform incoming operation với các local pending operations.

Mục tiêu:

Every client eventually converges to the same document state.

8. Storage Design

8.1 Vì sao không lưu full document sau mỗi edit?

Cách tệ:

Mỗi lần user gõ 1 ký tự -> lưu lại toàn bộ document

Nếu document 10MB và user chỉ gõ 1 ký tự, lưu lại 10MB là quá lãng phí.

Cách tốt hơn:

Mỗi edit -> lưu operation nhỏ
Định kỳ -> tạo snapshot

8.2 Operation Log

Operation log là append-only log.

Partition key có thể là:

document_id

Sort key có thể là:

version

Ví dụ DynamoDB/Cassandra schema:

DocumentOperations {
  partition_key: document_id
  sort_key: version
  operation_id
  user_id
  type
  position
  content
  base_version
  timestamp
}

Lợi ích:

  • Ghi nhanh.
  • Dễ replay.
  • Dễ audit.
  • Dễ support version history.
  • Phù hợp với write-heavy workload.

8.3 Snapshot

Snapshot chứa full document state tại một version cụ thể.

Ví dụ:

Snapshot {
  document_id: doc_123
  snapshot_version: 10000
  content: full document content at version 10000
  created_at
}

Snapshot không thay thế operation log. Nó giúp load nhanh hơn.

Nếu không có snapshot:

Load empty document
Replay operation 1 -> 1,000,000

Rất chậm.

Nếu có snapshot:

Load snapshot at version 990,000
Replay operations 990,001 -> 1,000,000

Nhanh hơn nhiều.

8.4 Safe Snapshotting

Khi tạo snapshot cho document đang được edit liên tục, snapshot phải gắn với một document_version cụ thể.

Ví dụ:

latest_version = 5000
snapshot_version = 5000

Trong lúc snapshot đang được tạo, edit mới vẫn có thể đến:

operation 5001
operation 5002

Những operation này sẽ nằm trong log segment mới sau snapshot.

Điều quan trọng:

Snapshot không được chứa nửa operation hoặc state không rõ version.

Invariant cần giữ:

snapshot(version = N) + operations(N+1 -> latest) = latest document state

8.5 Compaction

Nếu cứ giữ operation log mãi mãi, storage sẽ tăng vô hạn.

Sau khi có snapshot tại version N, các operation trước N có thể:

  • compact
  • compress
  • archive sang cold storage
  • hoặc discard nếu không cần full history

Ví dụ:

Snapshot at version 10000 exists
Operations 1..10000 can be compacted/archive
Operations 10001..latest remain hot

Tùy requirement về version history, audit, compliance mà quyết định xóa hay archive.


9. Redis Pub/Sub vs Kafka

Cả Redis Pub/Sub và Kafka đều dùng để truyền message giữa services, nhưng tính chất khác nhau.

Redis Pub/Sub

Redis Pub/Sub phù hợp cho low-latency real-time fanout.

Ưu điểm:

  • Đơn giản.
  • Latency thấp.
  • Phù hợp với cursor/presence/temporary update.
  • Phù hợp nếu document operation đã được lưu bền vững trong DB.

Nhược điểm:

  • Không persist message.
  • Consumer offline sẽ miss message.
  • Không replay được.

Kafka

Kafka phù hợp cho durable event stream.

Ưu điểm:

  • Persist event.
  • Consumer có thể replay theo offset.
  • Phù hợp cho audit, analytics, downstream service.
  • Phù hợp nếu event không được phép mất.

Nhược điểm:

  • Phức tạp hơn.
  • Operational overhead cao hơn.
  • Latency thường cao hơn Redis Pub/Sub.

Chọn cái nào?

Với collaborative document editing:

Actual document edit -> persist vào DB operation log
Real-time fanout -> Redis Pub/Sub có thể đủ

Vì DB đã là source of truth, Redis Pub/Sub chỉ cần deliver nhanh đến online clients.

Nếu cần downstream reliable processing, analytics, notification, hoặc event replay, có thể dùng Kafka.

Một câu trả lời tốt:

For real-time collaboration fanout, Redis Pub/Sub is enough if durability is handled by the operation log. 
For durable event streaming and replay, Kafka is a better fit.

10. Scaling to Millions of Connections

10.1 WebSocket không tự động giải quyết scale

WebSocket giúp giảm polling overhead, nhưng mỗi user vẫn giữ một long-lived TCP connection.

1 million users = 1 million open WebSocket connections

Không có nghĩa là cần 1 million servers.

Một WebSocket server có thể giữ nhiều connection.

Ví dụ:

1 server handles 20,000 connections
1,000,000 connections / 20,000 = 50 servers

Số thực tế phụ thuộc vào:

  • memory per connection
  • heartbeat frequency
  • message rate
  • fanout size
  • CPU cost for OT
  • network bandwidth
  • instance size
  • runtime/language

10.2 Horizontal Scaling

Ta scale Document Service theo chiều ngang.

Users
  -> Load Balancer
  -> Document Service 1
  -> Document Service 2
  -> Document Service 3
  -> ...

Mỗi Document Service giữ các WebSocket connection của chính nó.

10.3 Vì sao cần route theo document_id?

Với OT, tốt nhất là tất cả operation của cùng một document đi qua cùng một owner shard/service.

Invariant:

All operations for the same document are ordered by one owner service/shard.

Nếu User A, B, C cùng sửa doc_123, ta muốn:

User A -> Document Service 7
User B -> Document Service 7
User C -> Document Service 7

Như vậy Document Service 7 có thể:

  • apply OT theo một order rõ ràng
  • assign version tuần tự
  • persist operation
  • broadcast update

10.4 Consistent Hash Ring

Ta có thể shard document bằng consistent hashing trên document_id.

hash(document_id) -> hash ring -> owner Document Service

Ví dụ:

hash(doc_123) = 75

Hash ring:
0-30    -> Document Service A
31-60   -> Document Service B
61-100  -> Document Service C

doc_123 belongs to Document Service C

10.5 ZooKeeper / etcd / Consul

Hash ring metadata có thể được lưu trong coordination service như:

  • ZooKeeper
  • etcd
  • Consul

Những service này không xử lý request edit trực tiếp. Chúng chỉ lưu metadata như:

Which Document Service instances are alive?
Which hash range does each instance own?
What is the current hash ring version?

Mỗi Document Service watch/cache thông tin này.

Flow:

Client connects to any Document Service
Document Service computes hash(document_id)
Document Service checks cached hash ring
If this service owns the document -> accept connection
If not -> redirect client to owner service

10.6 Redirect Flow

Ban đầu load balancer có thể đưa client vào bất kỳ Document Service nào.

Client -> Load Balancer -> Document Service A

Nhưng doc_123 thuộc Document Service C.

Document Service A checks hash ring
doc_123 belongs to Document Service C
Document Service A redirects client to C
Client reconnects to Document Service C

Mục tiêu là đảm bảo tất cả user đang edit cùng document sẽ vào cùng owner service.


11. Cross-server Broadcast

Nếu hệ thống có nhiều WebSocket server, một edit ở Server A phải đến được user đang connect ở Server B.

Ví dụ:

User A editing doc_123 -> connected to Server A
User B viewing doc_123 -> connected to Server B

Khi User A edit:

1. Server A receives operation
2. Server A persists operation
3. Server A publishes event to topic doc_123
4. Server B is subscribed to topic doc_123
5. Server B receives event
6. Server B pushes update to User B

Mỗi WebSocket server chỉ nên subscribe các topic của document mà nó đang có user xem.

Không nên broadcast mọi event đến mọi server.

Good:
Server subscribes only to documents it currently serves.

Bad:
Every document event is sent to every WebSocket server.

Điều này giúp giảm fanout cost khi scale lớn.


12. Consistency and Reliability

12.1 Source of Truth

Source of truth cho document content là:

Document Operations DB + Snapshots

Không phải Redis Pub/Sub.

Redis Pub/Sub chỉ dùng để truyền event nhanh đến online clients.

12.2 Không ghi mọi event vào DB

Không phải mọi message đều cần lưu bền vững.

Data Persist DB? Storage
Insert/delete/update document Yes Operation DB
Cursor position No Redis TTL
Presence online/offline No Redis TTL
Typing indicator No Redis TTL
Heartbeat No Memory/Redis TTL
Snapshot Yes S3/Blob/DB
Permission Yes DB

Rule đơn giản:

If it affects final document content -> persist it.
If it is only live UI state -> Redis/memory with TTL is enough.

12.3 Avoiding DB + Pub/Sub Inconsistency

Vấn đề:

Service saves operation to DB
Service crashes before publishing event
Other clients do not receive update

Cách xử lý tốt là dùng transactional outbox.

Flow:

1. In one DB transaction:
   - write document operation
   - write outbox event

2. Background publisher reads outbox

3. Publisher sends event to Kafka/Redis Pub/Sub

4. Mark outbox event as published

Như vậy nếu service crash sau khi ghi DB, event vẫn còn trong outbox và có thể được publish lại.

Nếu dùng DynamoDB/Cassandra không hỗ trợ transaction rộng như RDBMS, có thể dùng pattern tương đương như:

  • write operation + event record cùng partition
  • stream/CDC từ operation table
  • idempotent publisher
  • version-based deduplication

12.4 Idempotency

Client hoặc publisher có thể retry, nên operation cần có operation_id.

operation_id = client_generated_uuid

Server dùng operation_id để deduplicate.

Nếu nhận cùng một operation nhiều lần, server không apply lại.


13. Failure Scenarios

13.1 WebSocket server crash

Nếu một Document Service crash:

1. WebSocket connections bị đóng
2. Client reconnect qua Load Balancer
3. Client gửi doc_id
4. Service mới check hash ring
5. Client được route về owner mới nếu ring đã rebalance
6. Client load latest snapshot + replay operations
7. Client tiếp tục editing

13.2 Redis Pub/Sub message lost

Nếu Redis Pub/Sub message bị mất, document vẫn không mất dữ liệu vì operation đã được ghi vào DB.

Client có thể recover bằng cách:

GET /docs/{docId}/operations?afterVersion=client_version

Hoặc khi reconnect, server gửi các operation còn thiếu.

13.3 Client offline

Nếu client offline trong lúc document thay đổi:

1. Client reconnect
2. Client gửi last_seen_version
3. Server gửi operations after last_seen_version
4. Client replay và catch up

13.4 Concurrent edits during snapshot

Snapshot phải gắn với version.

Snapshot version = N
Operations after N stay in operation log
Recovery = snapshot(N) + operations(N+1..latest)

Không được tạo snapshot không rõ version.


14. Database Choices

14.1 Document Metadata DB

Có thể dùng relational DB hoặc NoSQL tùy scale.

Metadata gồm:

  • document title
  • owner
  • permissions
  • latest_version
  • latest_snapshot_version
  • created_at
  • updated_at

Nếu cần query phức tạp về sharing/permission, relational DB có thể dễ hơn.

14.2 Operation DB

Operation log là write-heavy, partition theo document_id.

DynamoDB/Cassandra phù hợp vì:

  • write throughput cao
  • append-only workload
  • partition + sort key rõ ràng
  • scale ngang tốt

Schema:

PK = document_id
SK = version

Cần chú ý hot partition nếu một document cực kỳ nhiều người cùng edit. Khi đó có thể cần owner shard mạnh hơn hoặc split document theo block.

14.3 Redis

Redis dùng cho:

  • presence
  • cursor
  • typing
  • connection mapping
  • short-lived state
  • TTL data

Không dùng Redis làm durable storage cho document content.

14.4 Object Storage

S3/blob storage dùng cho:

  • snapshots
  • archived logs
  • large embedded assets/images
  • exported document versions

15. Optimizing Document Storage

15.1 Operation Log + Snapshot

Đây là optimization chính.

Every edit -> append operation
Every N operations/time interval -> create snapshot
Load document -> latest snapshot + replay recent operations

15.2 Chunk / Block-based Storage

Với document lớn, có thể chia document thành nhiều block/chunk.

Ví dụ:

document
  block_1
  block_2
  block_3

Nếu user sửa block_2, ta không cần rewrite toàn bộ document.

Lợi ích:

  • giảm write amplification
  • load từng phần document
  • dễ cache
  • phù hợp document rất lớn

15.3 Compression

Có thể compress:

  • old operation logs
  • snapshots
  • archived versions

Các operation cũ ít được đọc có thể chuyển sang cold storage.

15.4 Cache

Có thể cache latest rendered document hoặc recent snapshot trong Redis/memory.

Nhưng cache không phải source of truth.


16. Security and Access Control

Khi user mở WebSocket:

1. Validate auth token
2. Extract user_id from token
3. Check permission for doc_id
4. If role = viewer, reject edit operations
5. If role = editor/owner, allow edit

Không tin user_id do client gửi trong body.

Mọi operation nên được server attach user_id từ authenticated session.


17. Back-of-the-envelope Scaling

Giả sử:

10 million DAU
1 million concurrent users
average 5 active documents per 100 users at peak
each active editor sends 1 operation/sec while typing
cursor update throttled to 5-10 updates/sec max

Connection scaling:

1 million WebSocket connections
If one instance handles 20k connections
Need around 50 instances
Add buffer for failover/spike -> maybe O(100) instances

O(100) nghĩa là khoảng vài chục đến vài trăm instance, không phải chính xác 100.

Operation write scaling:

If 100k active editors
Each sends 1 operation/sec
Need ~100k writes/sec to operation log

Cursor/presence scaling:

Cursor updates are much more frequent
Do not write them to main DB
Use Redis TTL + pub/sub
Throttle/debounce cursor events

18. Interview Answer Summary

Một câu trả lời gọn có thể nói như sau:

I would design Google Docs as a real-time collaborative editor using WebSockets for bidirectional communication. 
Document edits are represented as operations, and the server uses Operational Transformation to order and transform concurrent edits. 
Actual document operations are persisted durably in an append-only operation log, while temporary real-time state like cursor, presence, and typing indicators are stored in Redis with TTL.

To make loading efficient, I would periodically create versioned snapshots. 
When a user opens a document, the system loads the latest snapshot and replays only operations after that snapshot. 
Older logs before a snapshot can be compacted or archived.

For scale, I would horizontally scale Document Service instances. 
Since all operations for the same document should be ordered consistently, I would route users by document_id using consistent hashing. 
The hash ring metadata can be stored in ZooKeeper/etcd/Consul, and each Document Service caches the ring. 
If a client lands on the wrong service, it is redirected to the owner service for that document.

For cross-server real-time fanout, Document Services use Redis Pub/Sub or Kafka. 
Redis Pub/Sub is simpler and low-latency for temporary real-time fanout, while Kafka is better if we need durable replayable events. 
The database remains the source of truth, so pub/sub is used for delivery, not durability.

19. Common Mistakes to Avoid

Mistake 1: Chỉ nói “use WebSocket” là scale xong

WebSocket giảm polling overhead, nhưng vẫn phải scale connection servers horizontally.

Mistake 2: Ghi cursor vào DB chính

Cursor thay đổi liên tục và không cần durable. Nên dùng Redis TTL.

Mistake 3: Không vẽ pub/sub layer

Nếu có nhiều WebSocket servers, phải có Redis Pub/Sub/Kafka để event từ Server A đến được user trên Server B.

Mistake 4: Lưu full document sau mỗi edit

Nên dùng operation log + snapshot.

Mistake 5: Snapshot không gắn version

Snapshot phải có snapshot_version, nếu không recovery sẽ dễ sai khi có concurrent edits.

Mistake 6: Truyền editor_id trong body

User identity nên lấy từ Authorization token hoặc WebSocket session.

Mistake 7: Dùng endpoint không RESTful

Nên dùng:

POST /docs

Không nên dùng:

POST /docs/create

20. Final Mental Model

Document content:
  durable operation log + versioned snapshots

Real-time transport:
  WebSocket

Conflict resolution:
  Operational Transformation

Temporary collaboration state:
  Redis with TTL

Cross-server delivery:
  Redis Pub/Sub or Kafka

Scaling:
  horizontal Document Service instances
  route by document_id using consistent hashing

Coordination:
  ZooKeeper / etcd / Consul stores hash ring metadata

Recovery:
  latest snapshot + operations after snapshot

Nếu chỉ nhớ một câu:

Google Docs scale tốt bằng cách lưu edit dưới dạng operation log, dùng OT để resolve concurrent edits, dùng WebSocket để push real-time updates, dùng Redis cho cursor/presence tạm thời, dùng pub/sub để fanout giữa servers, và dùng snapshot để load document nhanh.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí