এটা ২ পর্বের series এর প্রথম পর্ব। এই পর্বে আমরা বুঝবো — Google Docs এর মতো একটা collaborative editor কীভাবে কাজ করে: architecture, concurrent edit এর সমাধান (OT vs CRDT), storage strategy, আর real-time sync। Part 2 এ থাকবে offline editing, permissions (Zanzibar), fault tolerance আর scaling।
ধরো তুমি আর তোমার ৪ জন বন্ধু একসাথে একই Google Doc এ লিখছো। তুমি একটা paragraph টাইপ করছো, একজন উপরের heading edit করছে, আরেকজন comment add করছে, কেউ একজন offline এ আছে — internet নেই কিন্তু লিখেই যাচ্ছে। কিছুক্ষণ পর সবার screen এ হুবহু একই document দেখাচ্ছে। কোনো conflict নেই, কোনো লেখা হারায়নি, latency ১০০-২০০ms এর মধ্যে।
এটা যে কতবড় engineering feat সেটা প্রথমে বোঝা দরকার। এই post এ আমরা step-by-step দেখবো — Google Docs এর মতো একটা system কিভাবে design করা যায়। কোথায় কোন trade-off নিতে হয়, কোন algorithm কেন বেছে নেওয়া হয়, scale কিভাবে handle হয় — সব আলোচনা করবো।
Problem কী আসলে?
মূল challenge এক লাইনে: "একাধিক মানুষ একই সাথে টাইপ, delete, format করছে — সব change প্রায় instant সবার screen এ দেখাতে হবে, আর সবার কাছে শেষ পর্যন্ত একই document থাকতে হবে।"
এর ভেতরে অনেকগুলো subtle জিনিস লুকানো:
- Real-time sync — sub-200ms latency, না হলে collaboration "natural" feel দেবে না
- Offline editing — internet চলে গেলেও কাজ চলবে, ফিরে এলে merge হবে
- Global scale — millions of concurrent users, multiple regions
- Version history — কে কখন কী লিখেছে, সব track রাখা
- Permissions — কে দেখতে পারবে, কে edit করতে পারবে, কে শুধু comment করবে
এগুলো individually কঠিন। একসাথে কঠিনতর।
Requirements clarify করি
System design এ প্রথম কাজ — requirements ঠিক করা। তাড়াহুড়া করে architecture আঁকতে গেলে later refactor করতে হবে।
Functional Requirements
- Real-time multi-user editing — কয়েক জন একসাথে edit করতে পারবে
- Version history — প্রতিটা change record করতে হবে (কে, কখন)
- Document sharing — owner, editor, commenter, viewer — চারটা role
- Offline editing — local এ change queue করবে, online হলে sync হবে
- Comments & suggestions — inline comment, suggestion mode
Non-Functional Requirements
- Low latency — 100-200ms target
- High availability — 99.9% uptime
- Fault tolerance — server crash, network partition handle করতে হবে
- Durability — কোনো লেখা হারাবে না, multi-region replication
Interview tip — Scale number কেন দরকার?
শুধু feature list দিয়ে design শুরু করলে interviewer প্রথমেই ধরবে। কারণ system এর architecture পুরোপুরি নির্ভর করে কত বড় scale তার উপর। 1000 user এর জন্য একটা MySQL আর একটা server ই যথেষ্ট। 100 million user এর জন্য sharding, multi-region replication, caching layer — সব লাগবে।
তাই requirements clarify করার সময় কিছু assumption number দিয়ে দিতে হবে। যেমন:
- 10M DAU — DAU মানে Daily Active User, প্রতিদিন কতজন user system এ active থাকে
- 100 edits/min per active user — প্রতি user গড়ে minute এ কতগুলো edit operation generate করে (একটা keystroke = একটা edit ধরো)
- Document size 100KB — গড়ে একটা document কত বড়
এই তিনটা number থেকে derive করা যায় — peak QPS (Queries Per Second — পিক সময়ে প্রতি সেকেন্ডে কতগুলো request আসে) কত, storage কত লাগবে, bandwidth কত হবে, কয়টা server প্রয়োজন। এগুলো ছাড়া architecture decision (যেমন "Bigtable নাকি single Postgres") অর্থহীন।
বানানো number ও OK — তুমি তো আর Google এর internal data জানো না। Interviewer চায় তুমি reasonable assumption নিয়ে যুক্তি দিয়ে আগাও। "ধরি 10M DAU" বলে শুরু করলে problem নেই, যতক্ষণ পরের calculation গুলো ওই number এর সাথে consistent থাকে।
High-level Architecture
এক ঝলক দেখে নাও পুরো system কেমন দেখাবে:
┌─────────────┐ WebSocket ┌──────────────────┐
│ Client │ ←──────────────→ │ Load Balancer │
│ (Browser) │ └────────┬─────────┘
└─────────────┘ │
↓
┌───────────────────────┐
│ Collaboration Servers │
│ (OT/CRDT engine) │
└─────┬──────────┬──────┘
│ │
┌─────↓────┐ ┌───↓─────┐
│ Pub/Sub │ │ Storage │
│ (Kafka) │ │ Layer │
└──────────┘ └─────────┘
│
┌─────────────────┼──────────────────┐
↓ ↓ ↓
┌─────────┐ ┌──────────┐ ┌──────────┐
│Bigtable │ │ Spanner │ │ Colossus │
│(ops log)│ │(metadata)│ │ (blobs) │
└─────────┘ └──────────┘ └──────────┘
চারটা layer:
-
Client — browser বা mobile app। User input capture করে, local state রাখে, optimistic update করে (server response এর আগেই UI তে দেখায়)।
-
Collaboration Server — মূল brain। সব client এর operation নেয়, transform করে, broadcast করে।
একটু বিস্তারিত — এই server এর কাজ আসলে তিনটা stage:
-
Receive: Client থেকে WebSocket এর মাধ্যমে operation আসে। প্রতিটা operation ছোট JSON, যেমন
{type: "insert", position: 42, text: "hello", author: "user-A", op_id: "abc-123"}। Server প্রথমে validate করে — user এর কি edit permission আছে? Operation এর format ঠিক আছে? -
Transform (resolve conflict): এটাই সবচেয়ে কঠিন কাজ। দুজন user প্রায় একই সময়ে operation পাঠালে server OT (Operational Transformation) বা CRDT (Conflict-Free Replicated Data Type) algorithm চালিয়ে ঠিক করে — কোন operation আগে, কারটা পরে, এবং পরের operation এর position আগের operation এর effect সাপেক্ষে কি change হবে। উদাহরণ — A position 5 এ insert করলো, B position 3 এ delete করলো; তখন A এর insert position কে adjust করে 4 বানাতে হতে পারে। এই logic পরের section এ deep dive আছে।
-
Persist & Broadcast: Transform হওয়া operation কে দুটো জায়গায় পাঠানো হয় — (১) Storage layer এ append (Bigtable এর operation log, যাতে version history থাকে আর crash হলে recover করা যায়), (২) Pub/Sub এর মাধ্যমে subscribed client গুলোর কাছে broadcast (যারা ওই document open করে রেখেছে, তাদের সবার screen এ change real-time এ পৌঁছে যায়)।
এছাড়া server এর আরো কিছু গুরুত্বপূর্ণ দায়িত্ব আছে:
-
Leader election — মনে রেখো, system এ কিন্তু একটা collab server না, হাজার হাজার server চলছে (global scale এ)। তাহলে প্রশ্ন — একই document যদি ৫ জন user edit করে, আর তাদের request ৫টা আলাদা server এ যায়, তাহলে কী হবে? প্রতিটা server independently operation apply করবে, ফলে ৫ জায়গায় ৫ রকম document state তৈরি হয়ে যাবে — conflicting state, কেউ মিলবে না। এটা ঠেকাতে প্রতিটা document এর জন্য একটা specific server কে "leader" বানানো হয় (consistent hashing দিয়ে assign করা হয় —
hash(doc_id) → server X)। ওই document এর সব operation শুধু ওই leader server এর কাছেই যায়, leader sequentially process করে, সবার কাছে একই order এ broadcast করে। Leader crash করলে অন্য server (Raft/Paxos protocol দিয়ে) নতুন leader হয়। -
Presence tracking — কে কে এখন document open করে আছে, কার cursor কোন position এ, কে কোন paragraph select করেছে — এই ephemeral state track করা। এটা storage এ persist করা হয় না, server এর memory তে থাকে; user disconnect হলে clear।
-
Session management — WebSocket connection drop হলে immediately session expire করা ঠিক না (user হয়তো 5 second পর reconnect করবে)। তাই কিছুক্ষণ state ধরে রাখা হয় — pending operation buffer, last known cursor position, ইত্যাদি। কতক্ষণ ধরে রাখবে সেটা tunable trade-off (memory vs UX)।
Stateless রাখা হয় deliberately — মানে সব state Bigtable/Spanner এ থাকে, server এর memory তে শুধু transient cache। এতে যেকোনো server crash করলে অন্য server সেই document এর responsibility নিয়ে নিতে পারে।
-
-
Sync Layer — WebSocket connection, Pub/Sub (Kafka/Google Pub/Sub) দিয়ে fan-out। (নিচে "Real-time Sync: WebSocket আর Pub/Sub" section এ flow diagram সহ detail আছে।)
-
Storage — তিন ধরনের data, তিন রকম database:
-
Bigtable — operation log রাখার জন্য। প্রতিটা edit operation এখানে append হয়। Bigtable wide-column NoSQL, write-heavy workload এ extremely fast আর horizontally scalable — তাই millions of operation/sec handle করতে পারে। (নিচে "Storage Layer: Operation Log + Snapshots" section এ JSON schema আর snapshot strategy সহ detail।)
-
Spanner — metadata আর permissions এর জন্য। যেমন document এর title, owner, created_at, কোন user এর কী role (editor/viewer/commenter) — এসব। এই data critical, strong consistency দরকার (permission check ভুল হলে security breach), তাই Spanner — Google এর globally-distributed relational DB যা ACID transaction guarantee দেয় multi-region এ।
-
Colossus — file attachment, embedded image, video এর জন্য। এটা Google এর internal distributed file system (GFS এর successor)। Document এর ভেতরে যখন image embed করা হয়, image টা actually Colossus এ store হয়, document এ শুধু reference (URL/blob ID) থাকে। ছোট structured data কে large binary থেকে আলাদা রাখা — classic pattern।
-
আসল চ্যালেঞ্জ: Concurrent Edits
ধরো document এ আছে: "Hello"
- User A: position 5 এ
" World"insert করলো →"Hello World" - User B: একই সময়ে position 0 এর
Hdelete করলো →"ello"
দুজনের operation প্রায় একই সময়ে server এ পৌঁছালো। এখন কী হবে?
- A এর operation আগে apply করলে:
"Hello"→"Hello World"→ তারপর B এর delete →"ello World" - B এর operation আগে apply করলে:
"Hello"→"ello"→ তারপর A এর insert position 5 এ → কিন্তু এখন position 5 তো nothing! Document এর length মাত্র 4!
এটাই convergence problem। সবার কাছে শেষ পর্যন্ত same result থাকতে হবে — order যাই হোক।
এই সমস্যা solve করার দুটো ক্লাসিক approach আছে: Operational Transformation (OT) আর Conflict-Free Replicated Data Types (CRDT)।
Approach 1: Operational Transformation (OT)
OT এর মূল idea — operation কে অন্য concurrent operation এর সাপেক্ষে transform করো।
উপরের example এ:
- B এর delete (position 0) যখন A এর insert (position 5) এর সাথে concurrent ছিল, তখন server B এর operation কে A এর state এর জন্য transform করে। B এর delete যেহেতু A এর insert position এর আগে, A এর insert position 5 কে transform করে position 4 এ নামিয়ে আনে।
- Final result সবার কাছে:
"ello World"(length 9)।
OT এর basic transformation function
ছোট example — দুটা insert operation কে transform করি:
// Op1: position p1 এ text t1 insert
// Op2: position p2 এ text t2 insert (concurrent)
function transform(op1, op2) {
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position < op2.position) {
// op1 আগে, op2 এর position shift করতে হবে
return { ...op2, position: op2.position + op1.text.length };
} else if (op1.position > op2.position) {
// op2 আগে, op2 এর position অপরিবর্তিত
return op2;
} else {
// একই position — tiebreaker দরকার (client ID compare)
return op1.clientId < op2.clientId
? { ...op2, position: op2.position + op1.text.length }
: op2;
}
}
// insert vs delete, delete vs delete — প্রতিটার আলাদা rule
}
প্রতিটা operation pair এর জন্য আলাদা transformation function লিখতে হয়। 4 operation type হলে 16 combination। Complexity বাড়ে দ্রুত।
OT এর বৈশিষ্ট্য
| দিক | বিবরণ |
|---|---|
| Pros | Battle-tested (Google Docs, Etherpad use করে), bandwidth efficient |
| Cons | Transformation function লেখা কঠিন, প্রতিটা new operation type এ N² function |
| Centralized? | সাধারণত হ্যাঁ — একটা central server operation order ঠিক করে |
Approach 2: CRDT (Conflict-Free Replicated Data Types)
CRDT এর philosophy আলাদা — data structure এমনভাবে design করো যেন concurrent modification কখনো conflict না করে।
text এর জন্য সবচেয়ে common CRDT হলো RGA (Replicated Growable Array) বা Logoot/Treedoc। প্রতিটা character এর একটা unique ID থাকে — সাধারণত (replicaId, counter) tuple। Position numeric নয়, বরং dense fractional ordering ব্যবহার করা হয় — যেমন rational number বা list of integers।
Example: Logoot-style position
প্রথমে দুটো term বুঝে নাও:
-
Replica — document এর একটা independent copy। CRDT এ কোনো central server নেই; প্রতিটা client (browser, mobile app, offline laptop — যেখানেই document এর copy আছে) একটা replica। User A এর browser = replica A, User B এর browser = replica B। প্রতিটা replica নিজের copy তে independently edit করে, পরে অন্য replica দের সাথে sync হয়।
-
replicaId — প্রতিটা replica এর unique identifier। সাধারণত UUID, বা
user_id + device_idcombination, বা session start এ generate করা random string। উদ্দেশ্য — system এ চলমান প্রতিটা replica যেন একে অপর থেকে আলাদা করে চেনা যায়। দুটো replica এর কখনো একই replicaId হবে না (UUID collision practically impossible)।
এবার মূল trick — CRDT এ প্রতিটা character এর position একটা tuple: (numeric_part, replicaId)। numeric_part দিয়ে main ordering ঠিক হয়; numeric_part যদি দুজনের একই হয়ে যায় (collision), তখন replicaId দিয়ে tie break হয়। এই দ্বিতীয় slot টাই হলো trick — collision হলেও ordering deterministic থাকে।
Document: "Hello" (initial state, কোনো replica এখনো কিছু insert করেনি)
চরিত্র 'H' position: (0.1, _)
চরিত্র 'e' position: (0.2, _)
চরিত্র 'l' position: (0.3, _)
চরিত্র 'l' position: (0.4, _)
চরিত্র 'o' position: (0.5, _)
এখন A আর B (দুটো ভিন্ন replica) একই সময়ে 'H' আর 'e' এর মাঝে কিছু insert করতে চায়:
A insert করে 'X' → position calculate করে: (0.15, A)
// numeric_part 0.15 কেন? কারণ 0.1 আর 0.2 এর মাঝে যেকোনো number
// A এর replicaId tuple এর দ্বিতীয় slot এ বসে
B insert করে 'Y' → position calculate করে: (0.15, B)
// B ও একই 0.15 বেছেছে (দুজনে independently calculate করছে, তাই collision possible)
// কিন্তু replicaId আলাদা — তাই tuple আলাদা: (0.15, A) ≠ (0.15, B)
Sort করার সময় lexicographic compare:
(0.15, A) < (0.15, B) // numeric সমান, তাই দ্বিতীয় element compare — "A" < "B"
Final document: "H X Y e l l o"
↑ ↑
A আগে B পরে — সবার কাছে same order, deterministic।
মূল insight: position শুধু numeric হলে collision হলে কেউ ঠিক করতে পারবে না কে আগে। কিন্তু (numeric, replicaId) tuple হলে collision এর পরও ordering unique থাকে — প্রতিটা replica independently calculate করেই same final order এ পৌঁছায়, কোনো central coordinator ছাড়াই।
কোনো transformation লাগছে না — operation গুলো commutative, যেকোনো order এ apply করলে same result।
CRDT এর বৈশিষ্ট্য
| দিক | বিবরণ |
|---|---|
| Pros | Decentralized, peer-to-peer compatible, offline editing trivial |
| Cons | Metadata overhead (প্রতি character এ ID), tombstone (deleted character এর জন্য marker রাখতে হয়), range operation জটিল |
| Used by | Figma, Automerge, Yjs |
OT না CRDT — কোনটা কখন?
দুটোর কোনোটাই universally better না। কোনটা use করবে সেটা depend করে তোমার system এর constraint, deployment model আর product requirement এর উপর।
OT use করো যদি —
- Central server architecture আছে এবং থাকবে (সব operation একটা trusted server দিয়ে যায়)। OT এর transformation logic centralized environment এ সবচেয়ে clean কাজ করে।
- Bandwidth/storage critical — প্রতি character এ metadata overhead afford করতে পারো না। OT এর operation lean:
{type, position, content}, কোনো per-character ID নেই। - Mature ecosystem দরকার — production-ready library, war-stories, debugging tool। OT এর behind decades of work (Google Docs, Etherpad, ShareDB)।
- Document type জটিল কিন্তু operation type সীমিত (যেমন rich text editor — insert, delete, format)। তখন transformation function লেখা manageable।
CRDT use করো যদি —
- Peer-to-peer / decentralized sync দরকার — কোনো central server নেই, বা multiple server independently sync হবে। CRDT এর operation commutative, server coordination ছাড়াই converge করে।
- Offline-first অভিজ্ঞতা priority — user দিনের পর দিন offline থাকতে পারে, fresh sync এ massive divergence merge করতে হবে। CRDT এ এটা trivial; OT এ painful।
- Local-first software বানাচ্ছো (Linear, Notion-style sync engine, Automerge-based app) — data primary থাকবে user এর device এ, server শুধু relay।
- Operation type বাড়তে থাকবে — নতুন feature যোগ করলে OT এ প্রতিটার জন্য N² transformation function লিখতে হবে; CRDT এ data structure design ঠিক থাকলে operation automatically compose হয়।
- Mobile / unreliable network central use case — CRDT এর reconciliation simpler, retry-safe।
Quick comparison table
| Dimension | OT এ ভালো | CRDT এ ভালো |
|---|---|---|
| Architecture | Centralized | Decentralized / P2P |
| Bandwidth & storage | কম overhead | বেশি (metadata, tombstone) |
| Offline divergence | Painful | Trivial |
| New operation type যোগ | Quadratic effort | Linear (mostly) |
| Implementation maturity | Battle-tested | Newer, fast-evolving |
| Conflict resolution | Server-side, explicit | Built into data structure |
| Examples | Google Docs, Etherpad, ShareDB | Figma, Linear, Automerge, Yjs |
Real-world default
- নতুন product বানাচ্ছো 2026 এ? Default CRDT (Yjs বা Automerge দিয়ে শুরু করো)। Ecosystem এখন mature, offline + P2P scenario CRDT এ অনেক সহজ।
- Existing OT-based system আছে? Migration এর effort huge — সাধারণত OT এই থাকা যায়, যতক্ষণ scaling এ block না হয়।
- Hybrid approach ও আছে — কিছু system OT use করে document content এ, CRDT use করে metadata/cursor sync এ। One size fits all না।
Google Docs কী use করে?
Google Docs OT use করে। Historical reason — তারা যখন শুরু করেছিল (2006), CRDT তখন academic research stage এ ছিল, production-ready ছিল না। Centralized server model OT এর জন্য perfect ছিল। আজ scratch থেকে বানালে হয়তো CRDT বেছে নিত — কিন্তু legacy system migrate করার cost prohibitive।
Interview এ এই trade-off explicit বলা important। "I'd go with CRDT because peer-to-peer sync becomes possible, offline editing is more natural, and the library ecosystem (Yjs, Automerge) handles most of the heavy lifting" — এমন reasoned answer দিলে interviewer খুশি। Blanket "CRDT better" বা "OT better" বলো না — context matter।
Storage Layer: Operation Log + Snapshots
document কে কিভাবে store করবো? দুটো naive approach আছে — দুটোই খারাপ:
- শুধু final document store করি — version history নেই, conflict resolve করার তথ্য নেই।
- প্রতিটা version আলাদা করে store করি — storage explode করবে।
ভালো approach: append-only operation log + periodic snapshots।
Operation Log
প্রতিটা edit একটা operation হিসেবে log এ যায়:
{
"op_id": "uuid-...",
"doc_id": "doc-123",
"type": "insert",
"position": 42,
"content": "World",
"author": "user-A",
"timestamp": 1747800000000,
"parent_version": "v-789"
}
এটা immutable — কখনো modify হয় না, শুধু append হয়। Bigtable বা Cassandra এ store করা সহজ।
Snapshot
প্রতি N operation এ (বা প্রতি কিছুক্ষণ পর পর) পুরো document এর state save করা হয় — এটাই snapshot। Document load করার সময়:
- সবচেয়ে recent snapshot load করো
- তার পরের operation গুলো apply করো
- Current document পেয়ে গেলে
v0 v1 v2 ... v999 [snapshot] v1000 v1001 v1002 ...
↑
এখান থেকে শুরু করে latest পর্যন্ত replay
Snapshot frequency tune করা trade-off: ঘন ঘন snapshot = fast load, বেশি storage। কম snapshot = slow load, কম storage।
Version history
User চাইলে "আগের version এ revert করো" button চাপতে পারে — সেটা মানে operation log এ পিছনে গিয়ে সেই point পর্যন্ত replay করা। প্রতিটা keystroke এর হিসেব রাখলে storage বাড়বে, তাই Google Docs সাধারণত "meaningful pause" detect করে version checkpoint বানায়।
ছোট example
ধরো Alice একটা document লিখছে। প্রতিটা keystroke operation log এ যাচ্ছে, কিন্তু version checkpoint বানানো হচ্ছে শুধু "meaningful pause" এ (যেমন 30 second টাইপ না করা, বা bigger structural change হলে)।
সময় Operation Version checkpoint?
─────────────────────────────────────────────────────────────────
10:00:00 doc create করলো ✓ v1 (empty doc)
10:00:05 insert "Hello"
10:00:08 insert " world"
10:00:12 typing pause (30s) ✓ v2 ("Hello world")
10:02:30 Bob joined, insert " everyone"
10:02:45 Alice insert "!" at end
10:03:20 typing pause (30s) ✓ v3 ("Hello world everyone!")
10:05:00 Alice delete "everyone "
10:05:10 Alice insert "friends"
10:05:45 typing pause ✓ v4 ("Hello world friends!")
এখন Alice "File → Version history" এ ক্লিক করলে দেখবে:
v4 — 10:05:45 "Hello world friends!" (current)
v3 — 10:03:20 "Hello world everyone!" [Bob joined here]
v2 — 10:00:12 "Hello world"
v1 — 10:00:00 (empty)
Alice যদি v2 এ revert করতে চায় — system v1 snapshot load করে v2 পর্যন্ত operation replay করে "Hello world" পেয়ে যায়, current document সেটা দিয়ে replace হয় (নতুন একটা operation হিসেবে log এ যায়, পুরোনো history wipe হয় না)।
লক্ষ্য করো — keystroke level এ প্রতি character এর জন্য আলাদা checkpoint বানালে এই simple document এ ও 30+ version হয়ে যেত। "Meaningful pause" দিয়ে সেটা 4 এ নেমে এসেছে — practical history, কম storage cost।
Real-time Sync: WebSocket আর Pub/Sub
এই section এ বুঝবো — Alice keyboard এ একটা character টাইপ করলে, সেটা 100ms এর মধ্যে Bob এর screen এ কীভাবে পৌঁছায়। দুটো জিনিস জানা লাগবে: WebSocket (client আর server এর মধ্যে real-time channel) আর Pub/Sub (server থেকে অনেক client এ একসাথে message পৌঁছানোর system)।
আগে polling কেন কাজ করে না?
Real-time এর সবচেয়ে naive solution — client প্রতি কিছুক্ষণ পর পর server কে জিজ্ঞেস করবে "নতুন কিছু আছে?"। এটাকে বলে polling।
Client: "নতুন update আছে?" → Server: "না"
[100ms পর]
Client: "নতুন update আছে?" → Server: "না"
[100ms পর]
Client: "নতুন update আছে?" → Server: "হ্যাঁ, এই op"
সমস্যা: 100ms latency target এর জন্য প্রতি client প্রতি সেকেন্ডে ~10টা HTTP request পাঠাবে। 1 million concurrent user মানে server এ 10M requests/sec, যার 99% empty response। CPU, bandwidth সব waste। তাছাড়া প্রতিটা HTTP request এ TCP handshake + TLS handshake overhead — latency বাড়ে।
WebSocket — persistent bidirectional channel
WebSocket একটা protocol যেটা HTTP এর উপর handshake করে, তারপর connection টা খোলা রাখে — close না করে। একবার connect হয়ে গেলে দুদিক থেকেই data push করা যায়, কোনো নতুন request পাঠানো ছাড়াই।
[Initial HTTP handshake: "Upgrade to WebSocket?"]
Client ←────────── persistent TCP connection ──────────→ Server
(যতক্ষণ disconnect না হবে, ততক্ষণ খোলা)
এখন যেকোনো সময়:
Server → Client: "এই নতুন op apply করো" (push, polling ছাড়া)
Client → Server: "আমি একটা op generate করলাম"
মূল সুবিধা:
- Latency কম — handshake overhead একবারই, পরে শুধু data frame পাঠানো
- Server-initiated push সম্ভব — polling এর মতো client কে জিজ্ঞেস করতে হয় না
- Bidirectional — একই connection এ দুদিকে message যায়
প্রতিটা client document open করলে collab server এর সাথে একটা WebSocket connection establish করে। ওই connection টাই throughout session live থাকে।
Pub/Sub কী জিনিস?
WebSocket এ একটা সমস্যা — connection প্রতি client এ আলাদা। ধরো একই document 10,000 user open করে রেখেছে। Alice একটা character টাইপ করলো। সেই operation এখন 9,999 জনের কাছে পাঠাতে হবে।
Naive approach — collab server প্রতিটা WebSocket এ একটা একটা করে message পাঠাবে। 9,999 send। প্রতি operation এ এটা করলে server CPU আর network bandwidth সাথে সাথে dead।
সমাধান: Pub/Sub (Publish-Subscribe) pattern।
Pub/Sub explain
Pub/Sub একটা messaging architecture। তিনটা component:
- Publisher — যে message তৈরি করে পাঠায় (এখানে collab server)
- Subscriber — যে message receive করতে চায় (এখানে প্রতিটা client এর WebSocket handler)
- Broker / Topic — মাঝখানের system যেটা messages কে route করে (Kafka, Google Pub/Sub, Redis Pub/Sub, RabbitMQ — এসব tool)
কাজ করে এভাবে:
- Topic তৈরি হয় — যেমন প্রতিটা document এর জন্য একটা topic:
doc-events:doc-123 - যে যে client এই document open করেছে, তারা সেই topic এ subscribe করে — মানে broker কে বলে "এই topic এ message এলে আমাকে পাঠিও"
- Collab server operation generate হলে সেটা ওই topic এ publish করে — মানে broker কে বলে "এই message টা এই topic এ ছেড়ে দাও"
- Broker তার subscriber list দেখে — যারা subscribed, সবাইকে message fan-out করে দেয়
┌──────────────────────────┐
│ Topic: doc-events:doc-1 │
Collab Server ─publish→│ (Broker) │─fan-out→ Subscriber 1
│ │ ↘
└──────────────────────────┘ → Subscriber 2
↘
→ Subscriber N
কেন এটা ভালো:
- Server এর কাজ commodity — collab server এর শুধু publish করতে হয় একবার, broker বাকি কাজ handle করে
- Broker scalable — Kafka horizontal scale করতে পারে, billions of message/sec handle করতে পারে
- Decoupled — publisher জানে না কতজন subscriber, কে কে। নতুন client join করলে শুধু subscribe করলেই হয়
- Buffering — slow subscriber এর জন্য broker message queue করে রাখতে পারে, তাই slow client কারো জন্য system block করে না
Real-life analogy
YouTube subscription এর মতো ভাবো — একটা channel (topic) থেকে যত subscriber, সবাই upload হওয়া video (message) এর notification পায়। YouTuber কে প্রত্যেক subscriber কে আলাদা করে video পাঠাতে হয় না — YouTube এর system সেটা handle করে।
পুরো flow একসাথে
এখন একটা typing event এর full path দেখি। Alice টাইপ করলো 'X', Bob, Carol, Dave একই document দেখছে।
Alice (Client) Collab Server Pub/Sub Broker Bob, Carol, Dave
│ │ │ │
│ │ │ (সবাই subscribed │
│ │ │ "doc-events:doc-1") │
│ │ │ │
1. │─── WebSocket ───────→│ │ │
│ insert 'X' at 5 │ │ │
│ │ │ │
2. │ │ OT transform, │ │
│ │ Bigtable এ persist │ │
│ │ │ │
3. │ │── publish ───────────→│ │
│ │ "doc-events:doc-1" │ │
│ │ {op: insert 'X' @ 5} │ │
│ │ │ │
4. │←─── ack ─────────────│ │ │
│ │ │ │
5. │ │ │── fan-out ───────────→│
│ │ │ same message │
│ │ │ সবার WebSocket এ
│ │ │ │
6. │ │ │ Apply op
│ │ │ UI re-render
পুরোটা ~50-100ms এ হয়। Bob এর screen এ Alice এর typing প্রায় instant দেখায়।
Pub/Sub এর জন্য কোন tool?
| Tool | Strength | Best for |
|---|---|---|
| Kafka | High throughput, durable log | Operation log persist + replay |
| Google Pub/Sub | Fully managed, global | GCP-based product |
| Redis Pub/Sub | In-memory, ultra low latency | Ephemeral data (presence, cursor) |
| RabbitMQ | Flexible routing | Complex routing rule দরকার হলে |
Google Docs সম্ভবত internal Pub/Sub system use করে। Production এ অনেক system দুটো tool একসাথে use করে — Kafka durable operation log এর জন্য, Redis ephemeral presence/cursor sync এর জন্য।
Edge case: Client এর internet flaky হলে কিছু operation miss হতে পারে। তাই প্রতিটা operation এ
op_idথাকে — client reconnect এ server এর কাছ থেকে missing ops range request করতে পারে ("আমি op_id 500 পর্যন্ত পেয়েছি, এর পরেরগুলো পাঠাও")। Broker এ recent operations buffer থাকে এই scenario এর জন্য।
Part 1 এর শেষে
এই পর্বে আমরা একটা functioning collaborative editor কীভাবে কাজ করে সেটা পুরো trace করলাম:
- Requirements আর scale estimation কীভাবে করতে হয়
- চারটা layer এর high-level architecture (Client, Collab Server, Sync Layer, Storage)
- Concurrent edit এর core challenge — convergence problem
- দুটো classic সমাধান — OT (centralized, mature) আর CRDT (decentralized, P2P-friendly)
- কখন কোনটা use করবে — context-dependent decision
- Storage strategy — operation log + snapshots + version checkpoint
- Real-time sync mechanics — WebSocket protocol + Pub/Sub fan-out pattern
এই পর্যন্ত পড়লে তুমি interview এ এই question এর core part টা handle করতে পারবে। কিন্তু একটা production-grade Google Docs এর জন্য আরো অনেক কিছু লাগে — offline editing, fine-grained permissions, fault tolerance, global scale। সেগুলো নিয়ে Part 2 এ আলোচনা করবো।

