এটা ২ পর্বের series এর দ্বিতীয় পর্ব। Part 1 এ আমরা দেখেছি — requirements, architecture, OT vs CRDT (concurrent edit), storage layer, আর real-time sync (WebSocket + Pub/Sub)। এই পর্বে আমরা production-grade system এর বাকি কঠিন অংশগুলো dive in করবো: offline editing, permissions, fault tolerance, scaling।
Part 1 শেষ হয়েছিল real-time sync flow দিয়ে — মানে দুই online user এর মধ্যে character পৌঁছানো। কিন্তু real world এ user সবসময় online থাকে না, network সবসময় reliable না, server crash করে, document permission complex হয়, আর users millions জুড়ে ছড়ানো। এসব handle করার জন্য কী কী লাগে — সেটাই এই পর্বের focus।
Offline Editing — সবচেয়ে কঠিন part
ধরো তুমি train এ বসে document edit করছো। হঠাৎ tunnel এ ঢুকলো train — internet চলে গেলো। কিন্তু তুমি জানোই না, লিখেই যাচ্ছো। 30 মিনিট পরে tunnel শেষ, internet ফিরলো। এই 30 মিনিটে তোমার পাশাপাশি যারা একই document edit করছিল, তারা তো online ছিল — তারা অনেক কিছু change করেছে। এখন কী হবে?
এই scenario হলো collaborative editor এর সবচেয়ে কঠিন challenge। real-time conflict (যখন দুজন একই সাথে online আছে) সহজ — divergence ছোট, milliseconds মাত্র। কিন্তু offline conflict — divergence বিশাল, কখনো ঘণ্টার পর ঘণ্টা। এই গর্ত পূরণ করতে হয় দুটো step এ:
- Local-first mode — offline এ user যেন স্বাভাবিকভাবে কাজ চালিয়ে যেতে পারে
- Reconciliation — reconnect হলে দুই version কে এক করা (পরের subsection এ detail)
Local-first mode — internet ছাড়াও কাজ চলবে
মূল idea — document এর primary copy থাকবে user এর device এ, server এর কাছে না। Internet থাকলে server এর সাথে sync হবে। না থাকলেও app function করবে, যেন কিছুই হয়নি।
এই philosophy কে বলে "local-first software"। Google Docs অনেকাংশে এটা support করে, Linear/Notion আরো serious ভাবে করে। এর তিনটা মূল component:
১. Operation queue (IndexedDB তে)
User টাইপ করলেই operation generate হয়। Online থাকলে সেটা সাথে সাথে server এ চলে যেত। Offline এ — সেটা browser এর IndexedDB এ store হয়।
IndexedDB কী? Browser এর built-in persistent database। সাধারণ in-memory state (যেটা page reload এ চলে যায়) এর বিপরীতে, IndexedDB এ data disk এ লেখা থাকে। মানে — user laptop বন্ধ করে দিলেও, কাল সকালে app খুললে queued operation গুলো অক্ষত থাকবে।
Offline op queue (IndexedDB এ stored):
[
{ op_id: "abc-001", type: "insert", position: 5, text: "Hello", timestamp: ... },
{ op_id: "abc-002", type: "insert", position: 10, text: " world", timestamp: ... },
{ op_id: "abc-003", type: "delete", position: 0, length: 1, timestamp: ... },
...
]
প্রতিটা operation এ unique op_id থাকে — পরে reconcile এর সময় deduplication এর জন্য দরকার। (network flaky হলে এক operation দুইবার পাঠানো হয়ে যেতে পারে — op_id থাকলে server detect করতে পারে "এটা already processed"।)
২. Optimistic UI update
User এর experience স্বাভাবিক রাখতে হবে। মানে — টাইপ করলে character যেন সাথে সাথে screen এ দেখা যায়, server এর confirmation এর জন্য অপেক্ষা না করে।
এটাই optimistic update — "আমি ধরে নিচ্ছি operation সফল হবে, সেভাবেই UI update করছি"।
User keystroke 'X'
↓
1. Local document state এ 'X' insert (instant)
2. UI re-render — screen এ 'X' দেখা যায় (instant)
3. Operation queue এ push (instant)
4. Online হলে — server এ পাঠানো (background)
Offline হলে — শুধু queue এ অপেক্ষা
User এর কোনো ধারণাই নেই internet আছে কি নেই। App এর top এ সাধারণত একটা small indicator থাকে — "Online" / "Offline — changes saved locally" — যেন user চাইলে জানতে পারে।
Risk — কখনো optimistic assumption ভুল হতে পারে (যেমন reconcile এ conflict marker আসতে পারে)। কিন্তু 99% ক্ষেত্রে কাজ করে, আর responsive feel এর জন্য এই risk নেওয়া worth it।
৩. Reconnection detection
Browser এ network status track করার দুটো way:
১. navigator.onLine API
Browser এ built-in একটা property — navigator.onLine — যেটা true/false return করে। JavaScript থেকে যেকোনো সময় check করা যায়:
if (navigator.onLine) {
console.log("Online আছি মনে হচ্ছে");
} else {
console.log("Offline");
}
// Event listener ও আছে:
window.addEventListener('online', () => { /* online হলো */ });
window.addEventListener('offline', () => { /* connection গেলো */ });
দেখতে ভালো লাগছে — built-in, free, event-based। কিন্তু আসলে এটা যেটা বলে সেটা মূলত OS-level network interface এর state। মানে — তোমার laptop WiFi এ connected আছে কিনা, বা ethernet cable লাগানো আছে কিনা। কিন্তু WiFi এ connected থাকা মানেই internet আছে তা বলা যাবে না।
বাস্তব scenario যেখানে এটা ভুল বলে:
- তুমি cafe র WiFi এ connected আছ, কিন্তু ওদের router এর upstream cable কাটা —
navigator.onLine = trueবলবে, আসলে কোনো সাইট লোড হবে না - Hotel এর captive portal — login না করা পর্যন্ত internet নেই, কিন্তু WiFi connected —
onLine = true - VPN connect হয়ে গেছে কিন্তু VPN server unreachable —
onLine = true - ISP outage — modem এ link আছে, কিন্তু internet নেই —
onLine = true
উল্টোটাও হতে পারে — virtual machine বা VPN environment এ অনেক সময় OS confused হয়ে onLine = false বলে যদিও আসলে internet আছে।
বটম লাইন: navigator.onLine দিয়ে শুধু একটা hint পাওয়া যায়, ground truth না। শুধু এটার উপর depend করলে user অনেক সময় "আমি তো online, কেন কাজ করছে না" frustration পাবে।
২. Heartbeat ping
আসল ground truth জানার তরিকা — server কে নিজে জিজ্ঞেস করো। প্রতি কিছু সেকেন্ডে (typically 10-30s) একটা ছোট request পাঠাও — যদি response আসে, online; না এলে, offline।
setInterval(async () => {
try {
const res = await fetch('/api/ping', { method: 'HEAD' });
if (res.ok) setStatus('online');
else setStatus('offline');
} catch {
setStatus('offline'); // network error = offline
}
}, 15000);
এটা real test — actually data পৌঁছাচ্ছে কিনা সেটা verify করছে। সব captive portal, ISP outage, VPN issue catch হবে।
কিন্তু trade-off:
- প্রতি ping এ bandwidth + battery খরচ হয় (mobile এ বিশেষ করে)
- Interval এর মাঝে disconnect হলে কয়েক সেকেন্ড delay তে detect হবে
- Server এ load বাড়ায় (10K concurrent user × 4 ping/min = 40K req/min)
তাই production এ সাধারণত WebSocket connection নিজেই heartbeat হিসেবে use করা হয় — extra polling লাগে না। WebSocket এর ping/pong frame protocol native built-in:
Client → Server: PING frame (প্রতি 30s)
Server → Client: PONG frame (instant reply)
কোনো PONG না এলে timeout এর পর connection dead ধরে নাও।
৩. দুটো combination — best of both
Production system সাধারণত layered approach use করে:
- Fast hint —
navigator.onLineevent এ immediately UI তে offline indicator দেখাও (user feedback এ দ্রুত) - Real verification — WebSocket heartbeat দিয়ে actual connection state track করো
- Reconnect logic — connection drop detect হলে immediately reconnect attempt শুরু করো, exponential backoff এ (1s wait, fail হলে 2s, তারপর 4s, 8s, 16s ... max 60s) — server flooding না হয় যদি সমস্যা long-running হয়
এই combination এ user UI তে দ্রুত "Offline" badge দেখে, internal logic আসল truth জানে, আর reconnect smart ভাবে retry করে।
সাধারণত দুটোর combination ব্যবহার করা হয়। WebSocket connection drop হলে immediately offline mode trigger হয়; reconnect attempt চলতে থাকে exponential backoff এ (1s, 2s, 4s, 8s...)।
Reconnect হওয়া মাত্রই — IndexedDB থেকে queued operation গুলো এক এক করে server এ push হতে শুরু করে। তারপরই reconciliation এর পালা (পরের subsection এ detail)।
সব মিলিয়ে user এর journey
[Online, typing normally]
↓ Internet চলে গেলো
[Offline indicator দেখা গেলো — "Changes saved locally"]
↓ User টাইপ চালিয়ে যাচ্ছে (UI optimistic, queue জমা হচ্ছে)
[30 মিনিট পরে — Internet ফিরলো]
↓ "Syncing..." indicator
[Queued op গুলো server এ push হচ্ছে]
↓ Reconciliation (পরের subsection)
[Sync complete — normal collaboration mode]
এই পুরো flow seamless মনে হয় user এর কাছে। ভেতরে IndexedDB persistence, optimistic update, heartbeat, exponential backoff, queue replay — সব silently কাজ করে।
Reconciliation — দুটো version কে এক করার কাজ
ধরো Alice cafe তে বসে document edit করছিল, হঠাৎ WiFi চলে গেলো। ও খেয়াল করেনি, লিখেই যাচ্ছে। 30 মিনিট পরে WiFi ফিরলো। এই 30 মিনিটে দুটো জিনিস হয়েছে:
- Alice এর browser এ — Alice নিজে 50টা operation generate করেছে (offline queue এ জমা আছে)
- Server এ — Bob, Carol, Dave অন্যরা মিলে আরো 200টা operation apply করেছে। Document এখন অনেক বদলে গেছে।
দুটো জগৎ আলাদা হয়ে গেছে। Alice এর কাছে document এর যে state ছিল 30 মিনিট আগে, সেটার ভিত্তিতে ও 50টা operation লিখেছে। কিন্তু server এর current state সম্পূর্ণ ভিন্ন। Alice এর operation এর position number, reference — সব stale।
এই দুই জগৎ কে আবার মিলিয়ে এক করার process এর নামই reconciliation। সহজ ভাষায় — "তোমার offline change গুলো current online document এর সাথে fit করিয়ে দাও, যেন সবার কাছে এক document থাকে আর কারো কাজ হারিয়ে না যায়।"
Reconciliation কেন কঠিন?
হ্যাঁ, reconciliation কঠিন কাজ — শুধু "offline change গুলো server এ পাঠিয়ে দাও" — এত সহজ না। কঠিনতা চার দিক থেকে আসে:
১. Position number মিলবে না (stale references): Alice এর প্রতিটা operation এ position number আছে (যেমন "position 11 এ insert")। কিন্তু এই 11 টা ছিল 30 মিনিট আগের document এর সাপেক্ষে। এখন server এর document বদলে গেছে — সেই 11 এখন সম্পূর্ণ ভিন্ন জায়গা point করছে।
২. কেউ delete করে দিতে পারে যা Alice edit করছিল: Alice যে paragraph offline এ পরিমার্জন করছিল, Bob সেটাই server এ delete করে দিয়েছে। Alice এর edit এখন কোথায় বসবে? Delete-restore? Lost?
৩. Order ঠিক করা: Alice এর 50টা op আর server এর 200টা op — final document এ এগুলো কোন order এ যাবে যাতে সবার কাছে same result আসে? Causal ordering preserve করতে হবে।
৪. Performance: 50 × 200 = 10,000 transformation calculation (OT এ)। বড় divergence এ exponentially expensive।
Example: position number কেন stale হয়
প্রথম সমস্যাটা (position mismatch) সবচেয়ে common, সেটা দিয়ে বোঝা যাক।
আগের document (Alice offline হওয়ার সময়):
"Hello world"
Alice offline এ লিখেছে:
position 11 এ insert "!" → ও ভাবছে result হবে "Hello world!"
কিন্তু server এ এই 30 মিনিটে যা হয়েছে:
Bob 'world' delete করেছে।
Carol 'everyone' insert করেছে।
Server এর current document: "Hello everyone"
এখন Alice এর "position 11 এ '!' insert" — position 11 মানে কী এখন?
"Hello everyone" এ position 11 হলো 'n' এর পরে — মানে "Hello every!one"।
কিন্তু Alice তো চাইছিল string এর শেষে '!' দিতে — মানে actually position 14 এ যাওয়া উচিত।
এই position mismatch Alice এর সব 50টা operation এ ঘটছে — কোনোটা ঠিক জায়গায় বসবে না যদি না কেউ smart transformation চালায়। Bigger divergence = bigger mismatch = more transformation = more chance of subtle bug।
এই কারণেই reconciliation kernel-level hard problem — multiple concurrent edit, stale state, deterministic ordering, performance — সব একসাথে handle করতে হয়।
CRDT এ কেন সহজ
মূল কথা এক বাক্যে — CRDT এ position টা number না, একটা globally unique address। সেই address কখনো বদলায় না, তাই 30 মিনিট আগের address আর এখনকার address একই থাকে — কোনো mismatch ই হয় না।
CRDT এ trick — character গুলোকে আজীবন unique ID দাও
CRDT এর insight হলো — যদি প্রতিটা character এর identity document এর state এর উপর depend না করে, তাহলে আর কোনো adjust করতে হবে না।
তাই CRDT তে প্রতিটা character কে একটা tuple address দেওয়া হয়: (numeric, replicaId)। এই address character যখন তৈরি হয় তখনই assign হয়, আর কখনো বদলায় না — না document এ অন্য কিছু insert হলে, না delete হলে, না অন্য replica কিছু করলে।
ব্যাপারটা physical address এর মতো ভাবো। তোমার বাড়ির ঠিকানা "১২৩ লেক রোড"। পাশের বাড়ি ভেঙে নতুন apartment হয়েছে, রাস্তার অন্য পাশে নতুন বাড়ি বানিয়েছে — তোমার ঠিকানা একই থাকবে, "১২৩ লেক রোড"। তুমি কাউকে চিঠি পাঠাতে চাইলে শুধু এই ঠিকানা লিখলেই হবে, পাড়ার মোট বাড়ির গণনা করতে হবে না।
OT এর position number হলো "এই রাস্তায় ১১ নম্বর বাড়ি" — পাড়ার configuration বদলালে গণনা বদলায়। CRDT এর tuple হলো "১২৩ লেক রোড" — absolute।
Concrete trace
ধরো initial state ছিল "Hello world"। প্রতি character এর tuple এমন:
H: (0.1, _) e: (0.2, _) l: (0.3, _) l: (0.4, _)
o: (0.5, _) ' ': (0.55, _) w: (0.6, _) o: (0.7, _)
r: (0.8, _) l: (0.9, _) d: (0.95, _)
Alice offline এ যখন string এর শেষে '!' দিতে চাইলো, ও local এ ওটার tuple calculate করে — last character ('d' at 0.95) এর পরে কোনো একটা number, যেমন (0.99, alice)। এই tuple Alice এর IndexedDB queue এ জমা হলো।
এই 30 মিনিটে server এ Bob আর Carol অনেক কিছু করেছে — world delete, everyone insert। Server এর state এখন:
H(0.1) e(0.2) l(0.3) l(0.4) o(0.5) ' '(0.55) e(0.6,bob) v(0.61,bob) e(0.62,bob) r(0.63,bob) y(0.64,bob) o(0.65,carol) n(0.66,carol) e(0.67,carol)
লক্ষ্য করো — Bob আর Carol নিজেদের তৈরি character এ নিজেদের replicaId বসিয়েছে, আর Hello এর tuples (0.1 থেকে 0.55) অপরিবর্তিত।
এখন Alice online এলো। ওর queued op পাঠালো server এ:
op: insert '!' at (0.99, alice)
Server এই op apply করতে কী করে? শুধু character list এ যুক্ত করে, তারপর tuple দিয়ে sort করে:
H(0.1) e(0.2) ... e(0.67,carol) !(0.99,alice)
↑
Alice এর '!' tuple sort করে শেষে গেছে
Render: "Hello everyone!" — exactly যা Alice চেয়েছিল।
কী কী কঠিনতা auto-solve হলো
- ❌ Position recalculate করতে হলো না — Alice এর tuple আগে যা ছিল, এখনো তাই
- ❌ Server এ transformation function চালাতে হলো না — শুধু sort করো, এটাই sufficient
- ❌ Order matter করছে না — Alice এর op আগে আসুক বা Bob এর পরে, sort করলে final result same
- ❌ Bob, Carol এর অস্তিত্ব নিয়ে worry করতে হলো না — Alice ওদের সম্পর্কে কিছুই জানে না, তাও কাজ হলো
OT এর reconciliation যেখানে hundreds of carefully-crafted transformation function এর dance, CRDT এ এটা একটা sorted insert। এজন্যই offline-first product (Linear, Notion এর কিছু feature, Figma) CRDT এই বানানো হয়।
মজার fact: Google Docs OT use করে — তাই long offline session ও well-support করে না। কয়েক ঘণ্টা offline থেকে এসে ফিরলে অনেক সময় conflict marker দেখায়, বা latest server version এ overwrite হয়ে যায়। CRDT হলে এই সমস্যা থাকতো না।
Alice এর UI তে কী হয়?
Disclaimer: নিচের list টা একটা idealized UX — কোনো একটা well-designed collaborative editor এ এমন হওয়া উচিত। Google Docs নিজে এর সব কিছু hubahu করে না। আসলে Google Docs offline mode এ "Working offline" badge দেখায়, reconnect এ silently sync করে, irresolvable conflict এর ক্ষেত্রে সাধারণত last-writer-wins বা both-versions-merge করে — তবে full "Your version vs Server version" diff Google Docs দেখায় না (Git এর মতো)। তাই এটা product design এর reference, exact replica না।
Reconcile চলাকালীন ভালো UX হলো:
- Reconnect এর সাথে সাথে "Syncing your offline changes…" indicator দেখানো
- Background এ queued op গুলো server এ push হতে থাকে
- Server প্রতিটা accept করলে Alice এর local copy update হয় (Bob/Carol এর changes দেখা যেতে শুরু করে)
- কোনো irresolvable conflict থাকলে user কে দেখানো হয় ("Your version" vs "Server version" diff)
Sync শেষ হলে indicator সরে যায়, Alice আবার normal collaboration mode এ ফিরে আসে।
Concrete timeline example — A offline, B online, same position এ insert
এতক্ষণ সব theory। এবার একটা specific timeline trace করি — দেখি প্রতি second এ কী হচ্ছে আর শেষে সবার screen এ কী আসে।
Setup
ধরো User A আর User B একই document edit করছে।
t=1s A online, B online Document: "Hello world" (v0)
t=2s A offline হলো Document: "Hello world" (v0)
t=3s A insert 'X' at 5 A local: "HelloX world"
Server: "Hello world" (A এর op queue এ আটকা)
B screen: "Hello world"
t=4s B insert 'P' at 5 A local: "HelloX world" (A কিছু জানে না)
Server: "HelloP world" (v1)
B screen: "HelloP world"
t=5s A online হলো ← এখানেই reconcile
t=5s এর আগ পর্যন্ত A এর screen এ "HelloX world", B এর screen এ "HelloP world" — দুজনের কাছে আলাদা document। এটাই divergence।
t=5s এ যা ঘটে
A reconnect এর সাথে সাথে দুটো জিনিস parallel এ হয়:
1. A → Server: op_a1 = { insert "X" at 5, parent_version: v0, author: A }
2. Server → A: op_b1 = { insert "P" at 5, parent_version: v0, author: B }
(A miss করেছিল offline থাকার সময়)
Server দেখে — op_a1 আর op_b1 দুটোরই parent_version v0, দুটোই position 5, concurrent same-position insert। Tiebreaker দরকার।
ধরো convention — author_id lexicographically ছোট হলে সেই character আগে বসবে। A < B, তাই A এর X আগে, B এর P পরে। (এটা arbitrary deterministic rule। সব replica একই rule follow করলেই convergence guaranteed।)
Server এর কাজ
Server state এখন: "HelloP world" (B এর op already applied)
A এর op_a1 আসলো — concurrent, same position, A wins tiebreak।
Apply op_a1 to "HelloP world":
position 5 এ X insert → P position 6 এ shift
Result: "HelloXP world" (v2)
A এর browser এ যা হয়
A এর local state ছিল "HelloX world"। Server থেকে op_b1 এলো:
A local: "HelloX world"
Apply op_b1 (insert "P" at 5) — কিন্তু A এর local এ position 5 এ ইতিমধ্যে X আছে।
Tiebreak A < B, A এর X আগে — B এর P shift হয়ে 6 এ যাবে।
Transformed op: op_b1' = insert "P" at 6
Apply: "HelloXP world"
A এর screen: "HelloXP world" ✅
B এর browser এ যা হয়
B এর local state ছিল "HelloP world"। Server থেকে op_a1 এলো:
B local: "HelloP world"
Apply op_a1 (insert "X" at 5) — concurrent with B এর own op।
Tiebreak A < B, A এর X position 5 এ — P shift হয়ে 6 এ যাবে।
Apply: "HelloXP world"
B এর screen: "HelloXP world" ✅
Final convergence
A এর screen: "HelloXP world"
B এর screen: "HelloXP world"
Server: "HelloXP world"
সবাই same document এ converge হলো। কারো লেখা হারায়নি। A এর X আছে, B এর P আছে, শুধু order টা deterministic rule দিয়ে fix হয়েছে।
Tiebreaker rule উল্টো হলে?
ধরো convention "later author_id wins" — তাহলে B এর P আগে, A এর X পরে। Final: "HelloPX world"। দুটোই valid। মূল কথা — সব replica একই rule use করতে হবে। না করলে A এর কাছে "HelloXP" থাকবে, B এর কাছে "HelloPX" — divergence permanent।
CRDT হলে?
কোনো explicit tiebreaker logic server এ লিখতে হবে না। Tuple structure কাজ টা করে দেয়:
A এর X: (0.55, A)
B এর P: (0.55, B)
Lex sort: (0.55, A) < (0.55, B)
Final: ...o(0.5) X(0.55,A) P(0.55,B) ' '(0.6)...
Render: "HelloXP world"
A এর browser local sort করলে এই result। B এর browser local sort করলে এই result। কোনো server arbitration ছাড়াই দুজনের কাছে same document — এটাই CRDT এর সৌন্দর্য।
কঠিন edge case
User A offline ছিল, একটা paragraph লিখলো। User B online ছিল, ওই paragraph delete করে দিলো। Reconnect হলে কী হবে?
- Last-writer-wins (LWW): B এর delete win করে, A এর লেখা হারিয়ে যায় — খারাপ UX।
- Resurrect on edit: A এর edit মানে ও content টা চেয়েছিল — paragraph ফিরিয়ে আনো।
- Conflict marker দেখাও:
"<<<< A's version | B's version >>>>"— Google Docs এর "Show changes" এর মতো।
Production system গুলো সাধারণত combination ব্যবহার করে — heuristic দিয়ে guess করে কোনটা better, edge case এ user কে দেখায়।
Permissions: Zanzibar
প্রথমে শুনতে মনে হয় — "কে কী করতে পারবে" check করা তো trivial। একটা if-statement, database query, done। কিন্তু এটা trivial মনে হওয়াটাই trap। Massive scale এ এটাই একটা distributed system এর সবচেয়ে কঠিন challenge গুলোর একটা।
কেন কঠিন? কয়েকটা real scenario ভাবো:
- একটা Google Doc যেটা 5 মিলিয়ন user এর সাথে link share করা — প্রতিজনের প্রতি keystroke এ permission check করতে হবে
- একটা document যেটা একটা team এর সাথে share করা, team এর ভেতরে subteam, subteam এর ভেতরে individual user — nested groups
- কেউ team থেকে remove হলে সাথে সাথে সব document থেকে access lose হওয়া উচিত — কিন্তু cache এ stale permission থাকতে পারে
প্রতি permission check 10ms লাগলে, 1 billion check/sec মানে অসংখ্য server। Latency budget এর মধ্যে fit করানো trivial না।
Google এই সমস্যা solve করতে Zanzibar নামে একটা system বানিয়েছে — paper 2019 এ publish করা। এটা billions of permission check per second handle করে, latency সাধারণত 10ms এর নিচে। শুধু Docs না — Drive, Calendar, YouTube, Cloud — সব Google product এর authorization এটা দিয়ে হয়।
Relationship-based model
Traditional approach হলো role-based — user এর একটা role থাকে (editor, viewer), document এর সাথে role list match করো। Simple কিন্তু rigid — nested group, sharing inheritance handle করা কঠিন।
Zanzibar এর approach আলাদা — সব permission relationship হিসেবে express করা হয়। একটা permission মানে দুই entity এর মাঝে একটা relation, একটা tuple এর মধ্যে।
doc:doc-123#editor@user:alice
doc:doc-123#viewer@group:team-engineering
group:team-engineering#member@user:bob
Format: <object>#<relation>@<subject>। পড়ার তরিকা:
- প্রথম line: "doc-123 এর editor হলো alice" — Alice document edit করতে পারবে
- দ্বিতীয় line: "doc-123 এর viewer হলো team-engineering group" — পুরো team view করতে পারবে
- তৃতীয় line: "team-engineering এর member হলো bob" — Bob team এর member
লক্ষ্য করো — তৃতীয় tuple এ document এর কোনো reference নেই। শুধু group membership। এই decoupling ই trick।
Permission check = Graph traversal
প্রশ্ন: "Bob কি doc-123 দেখতে পারে?" Zanzibar এই question কে graph problem হিসেবে treat করে।
শুরু node: bob
লক্ষ্য: doc-123 এর viewer (বা উপরের কোনো role) পৌঁছানো
Step 1: bob কোন কোন group এর member? → team-engineering
Step 2: team-engineering কি কোনো document এর viewer/editor? → doc-123 এর viewer
Step 3: doc-123 পেয়ে গেছি, target match → হ্যাঁ, Bob দেখতে পারবে
এই simple example এ 2 step, কিন্তু real scenario আরো জটিল হতে পারে — group এর ভেতরে group, role hierarchy (editor automatically viewer), shared folder এর permission যা inheritance এর মাধ্যমে document এ আসে। সব graph traversal এ resolve হয়।
সুবিধা
- Express করা easy — "Marketing team এর সব member can view this doc" এক tuple এ লেখা যায়
- Nested group natural — যত গভীর nesting, ততই বেশি graph hop, কিন্তু same algorithm
- Audit করা সহজ — কে কোন document কেন access পেলো — graph trace দেখলেই বোঝা যায়
- Multi-product reuse — same Zanzibar instance Docs, Drive, Calendar — সব এ use হতে পারে
Scale এ কীভাবে?
Graph traversal expensive হতে পারে — বিশেষ করে deep nesting এ। 10ms latency এ billions of check serve করার জন্য Zanzibar কয়েকটা trick use করে:
- Precomputed cache — common check গুলোর answer আগেই compute করে রাখা।
- Selective denormalization — কিছু relation flat করে রাখা, query time এ traversal কম।
- Geo-distributed replica — user এর কাছের region এ data copy, latency কম।
- Tunable consistency — কিছু check এ stale OK (fast), কিছু এ strict (slow but accurate)।
- Zookies — consistency token, "এই version এর data দিয়ে answer" guarantee।
Bottom line: Zanzibar শুধু একটা library না, একটা purpose-built distributed system — যেটা শুধু "this user can do this thing" এর answer দিতে দিনে trillions of times invoke হয়। Authorization কে product feature এর rank থেকে infrastructure এর rank এ তুলে আনা।
Fault Tolerance — machine fail হলে কী হবে?
Large-scale distributed system বানালে একটা সত্য মাথায় রাখতে হয় — machine fail করবেই। হাজার হাজার server চললে প্রতিদিনই কোনো না কোনো server crash করে, network cable কাটে, datacenter এ আগুন লাগে, electricity যায়, কোনো region পুরোপুরি offline হয়ে যায়। Google এর scale এ এসব daily occurrence।
তাই system design এর সময় ধরে নিতে হয় — failure আসবে, প্রশ্ন শুধু কখন আর কীভাবে handle করব। User এর দিকে থেকে যেন কোনো data loss না হয়, service down না হয়, edit হারিয়ে না যায়। এই গ্যারান্টি দিতে পারা মানেই fault tolerance।
চারটা মূল mechanism আছে এই কাজে। নিচে quick overview:
| Mechanism | কী করে | কত খরচ |
|---|---|---|
| Multi-region replication | পুরো region down হলে সামলায় | Extra storage, cross-region replication latency |
| Leader-follower failover | এক server crash করলে অন্য নেয় | Leader election overhead, coordination |
| Idempotent operations | network retry safe রাখে | প্রতি op এর unique ID track রাখা |
| Periodic snapshot | crash এর পর দ্রুত recover | Snapshot storage |
প্রতিটা নিচে details এ।
Leader-follower failover
আগের section এ "leader collab server" এর concept দেখেছিলাম — প্রতিটা document এর একটাই server authoritative, সব operation ওই server দিয়ে যায় (যাতে conflicting state না হয়)। কিন্তু সেই leader যদি হঠাৎ crash করে? কেউ একজন কে দ্রুত দায়িত্ব নিতে হবে, না হলে ওই document এর জন্য system down।
সমাধান — প্রতিটা leader এর কয়েকটা follower server থাকে। Follower রা leader এর সব operation real-time এ replicate করে, যাতে leader die করলে তারা সাথে সাথে দায়িত্ব নিতে পারে।
Process:
- Leader regularly heartbeat পাঠায় followers কে — "আমি জীবিত আছি"
- কয়েক second heartbeat না এলে followers বুঝে leader dead
- Followers নিজেদের মধ্যে Raft/Paxos এর মতো consensus protocol দিয়ে নতুন leader elect করে
- নতুন leader দায়িত্ব নেয়, client রা reconnect করে সেখানে
পুরো failover ~1-5 second এ হয়। User এর কাছে এটা একটা small "Reconnecting..." moment হিসেবে দেখা যায়।
Split-brain risk
একটা subtle danger — যদি network partition হয়, মানে দুটো server group একে অপরকে দেখতে পাচ্ছে না, কিন্তু client রা দুটো group এর সাথেই connect পাচ্ছে। তখন প্রতিটা group ভাবতে পারে "অন্যটা dead, আমিই নতুন leader" — দুটো leader, দুটো diverging state। এটাকে split-brain বলে।
Quorum-based consensus এটা prevent করে। Rule: leader হতে হলে majority (যেমন 5 server এর 3) এর সাথে contact থাকতে হবে। Partition হলে দুই দিকে majority থাকতে পারে না — একদিকের server গুলো leader হতে পারবে না।
Idempotent operations
Network unreliable। Client একটা operation server এ পাঠালো, response এর জন্য wait করছে — হঠাৎ connection drop। Client জানে না — server আসলে operation receive করেছিল কি না।
দুটোই possible:
- Server receive করেনি (request mid-way drop)
- Server receive করেছে, process করেছে, কিন্তু response client পর্যন্ত পৌঁছায়নি
Client কী করবে? Naive solution — retry পাঠাও। কিন্তু server যদি প্রথমবার success ই করে থাকে, retry মানে duplicate apply — "Hello" এর জায়গায় "HelloHello" হয়ে যাবে।
সমাধান — idempotency। প্রতিটা operation এ একটা unique op_id থাকে (UUID typically)। Server প্রতিটা processed op_id এর track রাখে। Retry এলে duplicate ID দেখে চিনে — "এটা already processed, just ack again, কিছু apply করব না"।
Client: insert "Hello" at position 5, op_id=abc-123
Server: receive, apply, ack
[network drop, ack lost]
Client: retry — insert "Hello" at position 5, op_id=abc-123 ← একই ID
Server: "এই op_id এর কাজ আগেই হয়ে গেছে, just ack again"
User এর data সঠিক থাকে, কোনো duplicate বা loss নেই।
Multi-region replication আর snapshots
বাকি দুটো mechanism আগে discuss করা হয়েছে:
- Multi-region replication — Bigtable, Spanner globally replicate করে। একটা region পুরোপুরি down হলে অন্য region থেকে serve continue হয়।
- Periodic snapshot — operation log এর পাশাপাশি document এর full state checkpoint। Crash এর পর শূন্য থেকে সব op replay না করে snapshot থেকে শুরু করা যায়।
Scaling — millions of user কে handle করা
Single server এ collaborative editor চালানো সহজ। কিন্তু 100M user, প্রতিজনে minute এ 100 edit — মানে peak এ billions of operation/sec। একটা server এ এটা impossible। তাই scaling।
Scaling এর মূল idea একটাই — load টা ছড়িয়ে দাও অনেক machine এ। কিন্তু কীভাবে ছড়াবে সেটাই tricky। একটা document যদি multiple server এ split হয়, sync নষ্ট হবে। একটা user যদি ভিন্ন region এ চলে যায়, latency বাড়বে। এসব trade-off carefully balance করতে হয়।
Horizontal scaling — server বাড়াও, vertical না
দুটো scaling approach আছে:
- Vertical — same server এ বেশি CPU/RAM যোগ করা। সীমা আছে — একটা সময় machine এর hardware আর বাড়ানো যায় না।
- Horizontal — আরো server যোগ করা। সীমা নেই, কিন্তু coordination challenge।
Google Docs horizontal approach use করে। তিনটা trick:
১. Stateless collab server। Server এর memory তে কোনো long-term state নেই — সব Bigtable/Spanner এ। মানে যেকোনো server যেকোনো request handle করতে পারে। একটা server crash করলে নতুন server পুরো state Bigtable থেকে read করে নিতে পারে। Scaling এ এটা critical — যত server লাগে যোগ করা যায়, কারো জন্য কোনো dependency নেই।
২. Document-based sharding। আগের section এ দেখেছিলাম — প্রতিটা document একটা specific leader server এ assign হয়। কিন্তু কীভাবে assign হবে? Consistent hashing দিয়ে। hash(doc_id) → server_X। যত server বাড়ে, document গুলো evenly distribute হয়। একটা specific document এর সব operation ওই একটা server দিয়ে যায়, তাই locality + consistency দুটোই থাকে।
৩. Load balancer। Client প্রথমে load balancer এর কাছে আসে, সেখান থেকে appropriate collab server এ route হয়। WebSocket connection এর জন্য session affinity দরকার — একই client এর সব message একই server এ যাওয়া উচিত, যাতে state continuity থাকে।
Geographic distribution — latency কমানো
User আমেরিকা তে, server সিঙ্গাপুর এ — round-trip latency 200ms+। 100-200ms target এর জন্য এটা impossible।
সমাধান — edge server / regional cluster বানানো। বিভিন্ন region এ (US-East, US-West, Europe, Asia, ...) আলাদা cluster থাকবে। User তার নিকটতম region এ connect করবে, latency 20-50ms এ নেমে আসবে।
কিন্তু complication — user London এ, তার collaborator Tokyo তে। দুজনের data একই document এর। দুজন আলাদা region এ connected। কীভাবে sync হবে?
Cross-region replication। Background এ data সব region এ replicate হয়। ছোট eventual-consistency window থাকে (~100-200ms), কিন্তু same-region latency fast। trade-off accept করা হয় কারণ collaboration এ brief delay manageable।
Strong consistency কোথায় দরকার? Permission check হ্যাঁ — Alice যদি access হারায়, সাথে সাথে block হতে হবে, না হলে security breach। Document content এর জন্য eventual consistency OK — কয়েক ms পর Bob এর change Alice দেখলে issue না।
Caching — দ্রুত data access
প্রতি request এ Bigtable hit করা slow। তাই caching layer — memory তে frequently accessed document রাখা।
Hot document (এখন কেউ edit করছে এমন):
└─→ Memory cache এ + snapshot + Bigtable
↑
সব edit instant
Cold document (কেউ অনেকদিন open করেনি):
└─→ শুধু Bigtable
↑
first open হলে cache এ load হবে
Hot document active editing চলাকালীন cache invalidation tricky — প্রতিটা new operation এ cache update করতে হবে, race condition avoid করে। সাধারণত leader server এর memory তেই কাজ হয়, তাই serialized।
Trade-offs — কোনো "perfect" answer নেই
System design এ সব decision এ trade-off আছে। কোনো একটা magic combination নেই যেটা সবার জন্য best। Context এর উপর depend করে কোনটা ভালো।
| Trade-off | Side A | Side B |
|---|---|---|
| Consistency | Strong (slow but accurate) | Eventual (fast but may diverge briefly) |
| Algorithm | OT (mature, complex transformation) | CRDT (simpler model, metadata heavy) |
| Snapshot frequency | Frequent (fast load, more storage) | Sparse (slow load, less storage) |
| Replication | Sync (durable but slow write) | Async (fast write but possible data loss) |
কয়েকটা concrete example:
- Banking system — strong consistency must। অনেক slower OK, কিন্তু একটা টাকা ভুল জায়গায় গেলে disaster
- Social media feed — eventual consistency fine। 5 second পর friend এর post দেখলে কিছু যায় আসে না
- Google Docs — middle ground। Document content eventual (user বুঝবে না), permission strong (security)
Interview এ এই trade-off গুলো explicitly mention করা important। Interviewer চায় তুমি দেখাও — "আমি একটা optimal solution মুখস্থ করিনি; আমি situation analyze করে decision নিতে পারি।"
শেষ কথা
Google Docs দেখতে সহজ। ভেতরে — Operational Transformation এর math, Zanzibar এর authorization graph, Bigtable এর operation log, multi-region Spanner replication, WebSocket fan-out, idempotency layer, offline queue, conflict resolution heuristic। সবগুলো interconnected।
এই pattern গুলো শুধু Google Docs এ না — Figma, Notion, Linear, multiplayer game (Among Us, Agar.io), collaborative coding (Replit, VS Code Live Share) — সবগুলোই একই principles এর variation। Real-time multi-user state synchronization যেকোনো distributed system এর foundational problem।
Interview এ আসলে — requirements clarify করো, big picture আঁকো, hard problem এ deep dive (OT/CRDT, offline sync), trade-off explicit বলো, production concern (fault tolerance, idempotency, permissions) ভুলো না।
আর সবচেয়ে important — silent design করো না। উচ্চস্বরে reasoning explain করো। Interviewer দেখতে চায় তুমি কিভাবে ভাবো, শুধু কী answer দাও সেটা না।
Series wrap
দুই পর্ব মিলিয়ে আমরা cover করলাম:
| Part 1 | Part 2 |
|---|---|
| Requirements + scale estimation | Offline editing (local-first + reconciliation) |
| High-level architecture | Permissions (Zanzibar relationship model) |
| OT vs CRDT (concurrent edit) | Fault tolerance (leader election, idempotency) |
| Storage (op log + snapshots) | Scaling (sharding, geo-distribution, caching) |
| Real-time sync (WebSocket + Pub/Sub) | Trade-offs |

