โหมดมืด
บทที่ 5 — Worked Examples
ก่อนอ่าน — ต้องรู้อะไรมาก่อน?
บทนี้รวมทุก concept จาก บทที่ 0-4 — ถ้ายังไม่ได้อ่านบทก่อน ๆ กลับไปก่อน
ควรเข้าใจ:
- RADIO framework (บทที่ 0)
- Vertical / Horizontal scaling, LB, CDN (บทที่ 1)
- SQL/NoSQL, sharding, caching (บทที่ 2)
- Sync/Async, queue, pub-sub, WebSocket (บทที่ 3)
- Timeout, retry, circuit breaker, idempotency (บทที่ 4)
หลังจบบท คุณจะ:
- เห็น 5 system design ตัวอย่างเต็ม (URL shortener, Twitter, Uber, Netflix, WhatsApp)
- ใช้ RADIO framework กับ real case
- เปรียบเทียบ trade-offs
- พร้อมทำ interview ของจริง
วิธีอ่านบทนี้
ลองออกแบบเองก่อน อ่านเฉลย — กระดาษ หรือ Excalidraw
ขั้นตอน:
- อ่าน "Requirements" + "Non-Functional"
- ปิดเฉลย — set timer 30-45 นาที — ลองออกแบบเอง
- เปิดอ่านเฉลย — เปรียบเทียบ
- จดสิ่งที่ "พลาด" หรือ "ใหม่"
🟢 มือใหม่: ครั้งแรกอ่านเฉลยก่อนแล้วทำความเข้าใจ flow — รอบสอง (อีกสัปดาห์) ค่อยปิดเฉลยลองทำเอง การปิดเฉลยตั้งแต่ครั้งแรกอาจ frustrating เกินไปถ้าไม่เคยทำ system design มาก่อน
ทำแบบนี้ทุก example — interview จะรู้สึก "เคยทำมาแล้ว"
Example 1: URL Shortener (TinyURL / bit.ly)
R — Requirements
ขั้นแรกของ RADIO framework คือเก็บ requirement ให้ครบก่อนออกแบบ — แยกเป็น functional (ระบบต้องทำอะไรได้บ้าง) และ non-functional (scale, latency, availability) สำหรับ URL shortener สิ่งที่ต้องชัดคืออัตรา read:write และข้อจำกัดเรื่องการแก้/ลบ:
Functional
1. POST /shorten { url: "https://long..." } → returns short URL
2. GET /:code redirects to long URL
3. (Optional) Custom alias
4. (Optional) Analytics — click count, geo
5. (Optional) ExpirationNon-Functional
- 100M URLs/day created
- 10:1 read/write ratio (อัตราส่วนการอ่านต่อการเขียน — 10:1 หมายถึงอ่าน 10 ครั้งต่อเขียน 1 ครั้ง → 1B redirects/day)
- Redirect latency p99 < 100ms (p99 = "99th percentile" — 99% ของ request เสร็จในเวลานี้, 1% ช้ากว่า)
- 99.9% availability
- URLs ห้าม editable/deletable (10-year retention)Estimate Scale
ก่อนเลือก architecture ต้องประมาณตัวเลขก่อน — QPS (read/write), storage และ bandwidth ตัวเลขเหล่านี้บอกว่าต้อง shard ไหม ต้องมี CDN ไหม สำหรับ URL shortener การคำนวณนี้ชี้ว่า read หนักมาก (1B redirect/วัน) จึงต้องเน้น cache:
Writes (QPS — Queries Per Second):
- 100M new/day → 100,000,000 / 86,400 sec ≈ 1,157 → ~1,200/sec avg
- Peak (5x avg) → ~5,000/sec [ดู "ทำไม 5x" ในบทที่ 6 § Twitter calc]
Reads:
- 1B/day → 1,000,000,000 / 86,400 ≈ 11,574 → ~12K/sec avg
- Peak (~4-5x) → ~50K/sec
Storage (5 years):
- 100M × 365 × 5 = 182.5B → ~180B URLs
- Each: ~500 bytes (long URL + metadata)
- Total: 182.5B × 500 bytes ≈ 91 TB → ~90 TB
- → Sharded storage needed
Bandwidth (redirect response):
- 50K/sec × 500 bytes = 25,000,000 bytes/sec = ~25 MB/sec read
- Manageable with CDN (CDN รับโหลด >90% เพราะ URL ส่วนใหญ่ซ้ำ ๆ)A — Architecture
ขั้น Architecture วาดภาพรวมว่าระบบประกอบด้วยอะไรบ้างและ request ไหลยังไง — สำหรับ URL shortener หัวใจคือชั้น cache หลายระดับ (CDN + Redis) ที่ดูด read ส่วนใหญ่ก่อนถึง DB และ Kafka แยกงาน analytics ออกไปไม่ให้กระทบ path หลัก:
D — Data Model
ขั้น Data model ออกแบบ schema และ index — สำหรับ URL shortener ตารางหลักคือ map ระหว่าง short code กับ long URL (code เป็น primary key เพื่อ lookup เร็ว) แยกตาราง clicks สำหรับ analytics เพื่อไม่ให้ write หนักไปกระทบตารางหลัก:
sql
CREATE TABLE urls (
code VARCHAR(8) PRIMARY KEY, -- short code "abc123"
long_url TEXT NOT NULL,
user_id BIGINT, -- optional, for analytics
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ -- nullable
);
CREATE TABLE clicks ( -- analytics
id BIGSERIAL PRIMARY KEY,
code VARCHAR(8) REFERENCES urls(code),
clicked_at TIMESTAMPTZ DEFAULT NOW(),
user_agent TEXT,
ip INET,
referer TEXT
);
CREATE INDEX ON urls(user_id) WHERE user_id IS NOT NULL;Generate Short Code — 3 Options
หัวใจของ URL shortener คือ "จะสร้าง short code ที่ไม่ชนกันยังไง" — มี 3 วิธีหลักที่แลกกันระหว่างความง่าย, การกระจาย (ไม่ต้อง coordinate ข้าม server) และความสั้น/เดายาก ส่วนนี้เทียบทั้งสามเพื่อเลือกให้เหมาะ:
Option A: Random Base62 ⭐ (Recommended)
Base62 = 0-9, a-z, A-Z (62 chars)
6 chars Base62 = 62^6 = ~56.8B combinations
6 chars Base36 = 36^6 = ~2.18B combinations (lowercase+digits — บางเจ้าใช้)
8 chars Base62 = 62^8 = ~218T (overkill)
Algorithm:
1. Generate random 6-char code
2. Insert into DB (UNIQUE constraint catches collision)
3. On collision, retry (เพิ่มความยาวเมื่อ collision rate สูง)🧮 Birthday paradox — เมื่อไหร่ที่ collision เริ่มเจ็บ?
Random code ไม่ได้รับประกันว่าไม่ชน — collision เริ่มมีนัยสำคัญเร็วกว่าที่คิด (รายละเอียดสูตรอยู่ใน Wikipedia — Birthday problem สำหรับคนสนใจคณิตศาสตร์ มือใหม่อ่านแค่ตารางผลลัพธ์ก็พอ)
Space 50% collision ที่ ~โค้ด 1% collision ที่ ~โค้ด 36^6 ≈ 2.18B ~55K codes ~6.6K codes 62^6 ≈ 56.8B ~280K codes ~34K codes 62^7 ≈ 3.52T ~2.2M codes ~267K codes แปลว่า Base36 6 chars เริ่มชน "บ่อยพอจะรู้สึก" ตั้งแต่ระดับหมื่นโค้ด — ไม่เหมาะกับ scale จริง · Base62 6 chars ทนได้ถึงระดับแสน · 7 chars ทนถึงระดับล้าน
📚 Production reality: ที่ scale bit.ly / TinyURL (>10M code) random+retry มี collision เยอะเกินจะทำ DB-check ทุกครั้ง — ส่วนใหญ่ใช้ Snowflake-style (timestamp+worker+seq → Base62) หรือ Hi/Lo allocation จาก central counter แทน random+retry เหมาะกับ scale เล็ก (<10M codes) เท่านั้น
(ที่มาสูตร: Wikipedia — Birthday problem (เข้าถึง 2026-06) — generalized P(collision) ≈ 1 − exp(−k²/2N))
✅ Simple, distributed (ไม่ต้อง coordinate)
✅ No coordination needed (different servers OK)
✅ Short, opaque (security)
❌ Random + DB-check = +1 round-trip ต่อโค้ด (และอาจ retry ที่ tail latency)Alternatives เมื่อ random+retry ไม่ทัน
🟢 มือใหม่: ดูแค่ Option A (random Base62) ก็พอสำหรับ scale ทั่วไป — ตาราง alternatives ด้านล่างเอาไว้ดูเมื่อ throughput สูงเกินกว่า DB round-trip ต่อโค้ดจะรับไหว (เช่น >10K writes/sec sustained)
| Pattern | กลไก | เหมาะเมื่อ | ต้องระวัง |
|---|---|---|---|
| Snowflake-style counter ranges | แต่ละ worker จองช่วง ID เช่น node1 = 0–1M, node2 = 1M–2M (หรือ Twitter Snowflake timestamp+worker+seq → encode Base62) | scale สูง · อยาก guaranteed unique โดยไม่ต้อง DB-check | ต้อง coordinate worker-id ครั้งเดียวตอน boot · code คาดเดาได้ (ไม่ opaque) |
| Hi/Lo allocation (Hibernate Hi/Lo gen — ref doc) | service ไป "ขอบล็อก" จาก central counter เช่น "ให้ฉัน 1000 ID" → cache local → ใช้หมดค่อยขอใหม่ | DB อยู่กับ Postgres/MySQL อยู่แล้ว · อยาก minimize round-trip | ID หายเมื่อ service crash (ไม่ค่อย contiguous) · ยัง predictable |
| Content-hash + collision suffix | code = base62( SHA256(long_url) )[:6] ถ้าชน ต่อ -1, -2, ... | อยาก auto-dedupe (URL เดียวกัน → code เดิม) | predictable + ผู้ใช้ที่อยากแยก analytics ของลิงก์เดียวกัน 2 ครั้งทำไม่ได้ |
→ ส่วนใหญ่ production (bit.ly, TinyURL) ใช้ random Base62 + DB UNIQUE — ง่ายและพอ ส่วน Snowflake/Hi-Lo เหมาะเมื่อ throughput สูงกว่าที่ DB round-trip per code จะรับไหว
Option B: Counter + Base62
1. Global counter (auto-increment)
2. Encode to Base62: 1234567890 → "bv8FA"❌ Single point of contention (counter)
❌ Predictable (security concern — easy to enumerate)Option C: Hash of Long URL
hash(long_url)[:6]✅ Same URL = same code (auto dedupe)
❌ Collisions (must handle)
❌ Predictable→ Production: ใช้ Option A — random base62
I — Interface
ขั้น Interface กำหนด API contract ที่ชัดเจน — endpoint, request/response, status code สำหรับ URL shortener มี 3 endpoint หลัก: สร้าง short URL (POST), redirect (GET ที่คืน 302), และดูสถิติ การออกแบบ contract ให้ชัดช่วยให้ frontend/client ทำงานต่อได้:
http
POST /api/shorten
{
"url": "https://very-long-url.com/path/to/resource?param=value",
"custom_alias": "mylink" (optional)
}
→ 201 Created
{
"code": "abc123",
"short_url": "https://tiny.url/abc123",
"expires_at": null
}
GET /:code
→ 302 Found (หรือ 307 Temporary Redirect)
Location: https://very-long-url.com/...
Cache-Control: private, max-age=90
→ (async) record click event
GET /api/stats/:code
→ 200 OK
{
"code": "abc123",
"clicks": 1234,
"created_at": "2026-05-01T...",
"geo_distribution": { "US": 500, "TH": 300, ... }
}🔀 ทำไม 302 ไม่ใช่ 301 — trade-off browser cache vs analytics accuracy
🟢 TL;DR สำหรับมือใหม่: ใช้ 302 +
Cache-Control: private, max-age=N— ถ้าจำได้แค่บรรทัดเดียว จำบรรทัดนี้ก็พอ อ่านรายละเอียดต่อถ้าสนใจตาม RFC 9110 §15.4 —
301 Moved Permanentlyบอก client ว่า "ลิงก์นี้ย้ายถาวร เก็บ cache ได้ยาว ๆ" → browser และ proxy จะ cache อย่างก้าวร้าว (บางตัว cache แบบ "ตลอดไป" จน user ล้าง history) → ครั้งต่อ ๆ ไปที่ user คลิก short URL เดิม จะข้าม server ของเราเลยผลคือ:
Status code Browser caching Analytics accuracy เหมาะกับ 301 Moved Permanently aggressive (อาจถาวร) นับคลิกขาด — ครั้งแรกเท่านั้น static redirect ที่ไม่แคร์ analytics 302 Found ปกติไม่ cache (เว้นแต่ระบุ Cache-Control)นับคลิกครบ URL shortener ทั่วไป — default ที่แนะนำ 307 Temporary Redirect เหมือน 302 แต่ห้ามเปลี่ยน method (POST→POST) นับคลิกครบ API redirect ที่ต้องคง method ในทางปฏิบัติ vendor ใช้ status code ต่างกัน — TinyURL เคยใช้ 301 ส่วน bit.ly เปลี่ยนเป็น 302 (หรือ 301 +
Cache-Controlสั้น ๆ) แล้วแต่ช่วงและ use case เพื่อให้ browser cache สั้น ๆ ลด load แต่ยังเก็บ analytics ได้ — เนื่องจากพฤติกรรม cache 301 ระหว่าง browser ไม่ consistent (Chrome bug 2014 — 301 cached ignoring Cache-Control) ดังนั้น interview answer ที่ปลอดภัย: 302 +Cache-Control: private, max-age=Nแล้วค่อยอธิบายเหตุผลข้างต้น(เช็คพฤติกรรมจริงด้วย
curl -I https://bit.ly/...หรือ HEAD request — ค่าตาม vendor และวันที่)ตรวจ status ครั้งสุดท้าย: 2026-06
O — Optimizations
ขั้นสุดท้ายของ RADIO คือ optimize ตาม bottleneck ที่ระบุไว้ — สำหรับ URL shortener จุดสำคัญคือ cache redirect ยอดนิยม (CDN + Redis หลายชั้น), ทำ analytics แบบ async ไม่ให้บล็อก redirect และ shard DB เมื่อข้อมูลใหญ่เกินเครื่องเดียว:
1. Cache Hot Redirects
[Client] → [CDN edge]
↓ cache HIT
return 302 (no app touch)
↓ cache miss
[App] → [Redis]
↓ HIT → return + populate CDN
↓ miss
[DB]
↓ return + populate cacheโค้ดในบทนี้เป็น pseudo-code เน้นไอเดีย ไม่ต้องอ่านออกทุกบรรทัด — จับ flow ตาม comment ก็พอ
typescript
async function redirect(code: string): Promise<string | null> {
// ชั้น 1 (L1): ลองหาใน Redis cache ก่อน — เจอก็คืนเลย (เร็วสุด)
let longUrl = await redis.get(`url:${code}`);
if (longUrl) return longUrl;
// ชั้น 2 (L2): ไม่เจอใน cache — ไปถาม Database
const row = await db.queryOne(
'SELECT long_url FROM urls WHERE code = $1',
code
);
if (!row) return null;
// เก็บผลลง cache ไว้ใช้ครั้งหน้า (อายุ 86400 วินาที = 1 วัน)
await redis.setex(`url:${code}`, 86400, row.long_url);
return row.long_url;
}2. Analytics — Async via Queue
ไม่ต้อง block redirect เพื่อ track click:
typescript
async function redirect(code: string): Promise<Response> {
const longUrl = await getLongUrl(code);
if (!longUrl) return notFound();
// Async — DON'T await
queue.publish({
type: 'click',
code,
timestamp: Date.now(),
ip: req.ip,
userAgent: req.headers['user-agent']
});
return redirect302(longUrl); // 302 + Cache-Control: private, max-age=N (ดูเหตุผลในย่อหน้า trade-off ด้านบน)
}Worker:
- Batch insert clicks (every 1 sec หรือ 1K events)
- Update aggregates (hourly/daily)
3. Sharding
180B rows → ใหญ่เกิน single DB
→ Shard by code hash:
- hash(code) % N → shard
- Reads: lookup by code → know which shard
- Writes: randomly distributed4. CDN for Long-tail
Most URLs accessed 1-2 times
แต่ ~1% URLs = 90% of traffic (heavy tail)
CDN caches popular URLs → ~80% redirects ไม่ hit appBottleneck + Scale Plan
ส่วนนี้สรุปว่าระบบจะเจอคอขวดตรงไหนเมื่อ scale และแก้ยังไง — ตารางจับคู่ bottleneck (DB write, hot read, analytics write) กับวิธีแก้ ทำให้เห็นภาพว่าควรลงทุนเรื่องไหนก่อนเมื่อ traffic โต:
| Bottleneck | Solution |
|---|---|
| DB write (create URL) | Sharding by code |
| Hot URL reads | CDN + Redis cache |
| Click analytics writes | Async queue + batch insert |
| Counter contention (Option B) | Per-server batch (claim 1000 IDs) |
Final Architecture
รวมทุกการตัดสินใจเป็นภาพสถาปัตยกรรมสุดท้ายของ URL shortener — CDN + LB + app server + Redis cache + Postgres sharded + Kafka/worker สำหรับ analytics เปรียบเทียบกับภาพแรกจะเห็นว่าเพิ่ม sharding และ analytics pipeline เข้ามาหลังวิเคราะห์ bottleneck:
Example 2: Twitter / X (Microblog)
R — Requirements
เริ่ม RADIO ที่ requirement เช่นเดิม — Twitter เป็นเคสคลาสสิกที่ read หนักกว่า write มาก (100:1) และยอม eventual consistency ได้ จุดนี้สำคัญเพราะมันจะนำไปสู่การเลือก timeline strategy ทีหลัง:
Functional
- User post tweet (280 chars)
- User follow other users
- Timeline: tweets from people user follows
- Like / retweet
- SearchNon-Functional
- 300M DAU
- Each user posts avg 2 tweets/day → 600M tweets/day
- Read/Write = 100:1 (read-heavy!)
- Timeline load < 200ms p99
- Eventual consistency OK
- Tweets immutable (mostly)Estimate
ประมาณการตัวเลขของ Twitter — write QPS ไม่สูงมาก (~7K) แต่ timeline read QPS มหาศาล (~350K avg, peak 1.5M) ตอกย้ำว่าต้อง cache แบบสุด ๆ ส่วน storage ระดับร้อย TB ต้อง shard:
Tweet write QPS: ~7K avg, ~35K peak
Timeline read QPS: ~350K avg, ~1.5M peak
Storage:
- 600M tweets/day × 200 bytes = 120 GB/day
- Per year: ~44 TB
- 5 years: ~220 TB
→ Read-heavy → must cache aggressively
→ Storage: shardedA — Architecture
สถาปัตยกรรม Twitter แตกเป็นหลาย service ตาม domain (tweet, timeline, follow, search, media) โดยมี timeline service + Redis เป็นพระเอกที่เก็บ feed คำนวณไว้ล่วงหน้า และ Kafka แยกงานหนัก (fanout, index, notification) ออกจาก path หลักแบบ async:
D — Data Model
schema ของ Twitter มีตารางหลัก users, tweets, follows, likes — จุดสำคัญคือ "shard key" ของแต่ละตาราง (tweets ตาม user_id, follows ตาม follower_id, likes ตาม tweet_id) ที่เลือกตาม pattern การ query เพื่อให้ข้อมูลที่อ่านพร้อมกันอยู่ shard เดียวกัน:
sql
-- Users
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tweets (sharded by user_id)
CREATE TABLE tweets (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
text VARCHAR(280) NOT NULL,
media_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ON tweets(user_id, created_at DESC);
-- Follows (sharded by follower_id)
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (follower_id, followee_id)
);
-- Likes (sharded by tweet_id)
CREATE TABLE likes (
tweet_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (tweet_id, user_id)
);Timeline Strategy — ส่วนที่ยากที่สุด
ตรงนี้คือ "interview gold" — เป็นปัญหาที่ทุกคนถาม:
Approach 1: Pull-based (Fanout-on-Read)
User เปิด timeline:
1. Get list of users they follow (e.g., 1000 คน)
2. For each, query latest tweets
3. Merge + sort by time
4. Return top 50
✅ No write amplification
✅ Always fresh
❌ Slow for users following many (1000 queries!)
❌ Bad for read-heavyApproach 2: Push-based (Fanout-on-Write) ⭐
User posts tweet:
1. Insert into tweets table
2. Get list of followers (10K คน)
3. For each follower, append tweet to their feed cache (Redis)
User opens timeline:
1. Get feed from Redis (precomputed list of tweet IDs)
2. Fetch tweets in bulk
3. Return
✅ Fast read (single Redis lookup)
❌ Write amplification (1 tweet × 10K followers = 10K writes!)
❌ Wasted if some followers never checkApproach 3: Hybrid (Twitter's Real Solution) ⭐⭐
For most users (< 1M followers): Push
For "celebrities" (> 1M followers): Pull
When user loads timeline:
1. Get fanout feed (Redis) for normal follows
2. Pull tweets for celebrity follows (query their tweets)
3. Merge + sort + returnทำไม Hybrid:
Celebrity tweet:
- 100M followers → 100M writes (too expensive!)
- Solution: ไม่ fanout — followers pull when load
Normal user:
- 100 followers → 100 writes (fine)
- Solution: fanout — fast read for followersInsight ที่ทำให้ interview score สูง: ระบุ trade-off + propose hybrid
Cache Strategy
เนื่องจาก Twitter read หนักมาก cache คือหัวใจ — ส่วนนี้ระบุว่าอะไรควรอยู่ใน Redis (profile, tweet, feed ที่คำนวณไว้, trending) พร้อม TTL ที่เหมาะ การเลือก cache ให้ครอบคลุม hot data คือสิ่งที่ทำให้ timeline โหลดได้ใน <200ms:
Hot data in Redis:
- User profile (per user, 1h TTL)
- Tweet (1 day TTL)
- User feed (top 500 tweets per user)
- Follower list (small users only)
- Trending hashtags
- Search suggestionsSharding
ข้อมูลของ Twitter ใหญ่เกินเครื่องเดียว ต้อง shard — แต่เลือก shard key ยากเพราะ pattern การ query ต่างกัน ส่วนนี้อธิบายเหตุผลเบื้องหลังการเลือก shard key ของแต่ละตาราง และจัดการ "celebrity" (ที่ทำให้ fanout ระเบิด) แยกออกมา:
Tweets — shard by user_id
- User's tweets all on 1 shard (efficient for "user's tweets")
- Cross-shard query: timeline (need data from many users)
- Solution: hybrid approach + feed cache
Users — shard by user_id
Follows — shard by follower_id
- "Who do I follow" — 1 shard query
- "Who follows me" — needs cross-shard scatter-gather (or separate table)
(scatter-gather = "ยิงถามทุก shard พร้อมกัน แล้วรวมผลกลับมา" — ช้าเพราะต้องรอ shard ที่ช้าสุด)
Celebrity tweets — separate (don't fanout, query on demand)Scale Numbers
ปิดท้ายเคส Twitter ด้วยตัวเลขที่แปลงการออกแบบเป็นจำนวนเครื่องจริง — กี่ shard, กี่ replica, ใช้ระบบอะไรสำหรับ media/search/real-time ตัวเลขเหล่านี้คือสิ่งที่ interviewer อยากเห็นว่าคุณคิดถึง capacity จริง:
- 35K writes/sec → ~10 DB shards (each 3.5K writes/sec)
- 1.5M reads/sec → mostly cached, ~30K reach DB → 5 replicas/shard
- Image storage → S3 + CloudFront CDN
- Search → Elasticsearch cluster (10+ nodes)
- Real-time updates → WebSocket (push notifications)Example 3: Uber / Ride-Hailing
R — Requirements
Uber เป็นเคสที่ต่างจากสองอันแรกตรงที่มี requirement "real-time" หนักมาก — ตำแหน่ง driver อัปเดตทุก 4 วินาที, match ต้องเสร็จใน 5 วินาที และ availability ต้องสูงมากเพราะ ride เป็นเรื่องคอขาดบาดตาย เก็บ requirement ให้ครบก่อนจึงสำคัญ:
Functional
- User opens app → see nearby drivers
- Request ride
- Match with driver (matching algorithm)
- Track ride (real-time location)
- Pay after ride
- RatingNon-Functional
- 100M users globally
- 10M rides/day
- Match in < 5 sec
- Real-time location (every 4 sec from driver)
- 99.99% availability (rides are critical!)
- Multi-region (low latency for users)Estimate
ตัวเลขที่น่าตกใจของ Uber คือ location update ~1.25M ครั้ง/วินาที (driver 5M คน × ทุก 4 วิ) — มากกว่า ride QPS หลายพันเท่า นี่คือเหตุผลที่ location ต้องอยู่ใน in-memory store (Redis) ไม่ใช่ DB ปกติ:
Rides: 10M/day = ~120/sec avg, ~500/sec peak
Active drivers: ~5M
Location updates: 5M × 1/4s = 1.25M updates/sec !!
Storage:
- Rides: 10M × 5 KB/ride = 50 GB/day
- Driver locations: high turnover, not persistent (Redis)
- Maps: GB-scale tile cacheA — Architecture
สถาปัตยกรรม Uber มีจุดเด่นที่ WebSocket gateway (สำหรับ real-time location/tracking) แยกจาก HTTP gateway, location service ที่ใช้ geo index (Redis Geo/H3) แทน DB ปกติ และ matching service ที่จับคู่ rider กับ driver งานหนัก (pricing, payment, ML) ยกไป async ผ่าน Kafka:
Key Challenge: Geo-spatial Matching
หัวใจที่ยากที่สุดของ Uber คือ "หา driver ที่อยู่ใกล้ rider เร็ว ๆ" บนแผนที่ทั้งโลก — แก้ด้วยการแบ่งแผนที่เป็น cell (Uber ใช้ H3 แบบหกเหลี่ยม) แล้ว query เฉพาะ cell รอบ ๆ rider แทนที่จะคำนวณระยะกับ driver ทุกคน ส่วนนี้สอนทั้งแนวคิดและ implementation ด้วย Redis Geo:
Approach: Geo-hash + Grid (Uber's H3 ⭐)
1. Divide map into cells (grid)
- QuadTree หรือ H3 (Uber's open source — hexagonal!)
- Each cell ~100m × 100m
2. Drivers ส่ง location:
- Lat/long → cell ID
- Update cell's "drivers" set
3. Rider requests ride:
- Compute rider's cell + neighbors
- Query "drivers in these cells"
- Filter by distance + availability + service type
- Match with nearestRedis Geo (Simple Implementation)
typescript
// Driver sends location
await redis.geoadd('drivers:active', longitude, latitude, driverId);
// Rider requests
const nearby = await redis.georadius(
'drivers:active',
riderLongitude, riderLatitude,
1000, 'm', // within 1km
'WITHCOORD', 'WITHDIST', 'COUNT', 20, 'ASC'
);Matching Algorithm
1. Get N nearby drivers (e.g., 20)
2. Filter:
- Online + available
- Service type matches (UberX, Pool, etc.)
- Rating > threshold
- Min distance > config
3. Estimate ETA for each
4. Send ride request to nearest (or best mix)
5. Wait for driver accept (10 sec)
6. If declined: try next driver
7. Timeout: try larger radiusReal-time Communication
Uber ต้องส่งตำแหน่ง driver ให้ rider เห็นแบบ real-time ขณะ ride ดำเนินอยู่ — ใช้ WebSocket connection ค้างไว้ต่อผู้ใช้ที่ active แล้ว publish ตำแหน่งผ่าน topic ของ ride นั้น ส่วนนี้อธิบายวิธีจัดการ connection จำนวนมากและ pub-sub ระหว่าง gateway:
WebSocket persistent connection per active user:
Rider connection:
- Subscribe to topic: "ride:123"
- Receives: driver location updates, status changes
Driver connection:
- Subscribe to: "driver:456:requests"
- Receives: incoming ride requests
Backend:
- Trip service publishes events
- Routed to relevant connections (via Redis pub-sub or Kafka)Sharding
ปิดท้ายเคส Uber ด้วยกลยุทธ์ sharding — แต่ละ entity (ride, user, driver) shard ตาม id ของตัวเอง และข้อมูล geographic แบ่งตาม region เพื่อให้ผู้ใช้แต่ละพื้นที่ถูกบริการจาก shard ใกล้ตัว (latency ต่ำ + ตรง data residency):
Rides — shard by ride_id (random distribution)
Users — shard by user_id
Drivers — shard by driver_id
Geographic data — shard by region (Asia, EU, Americas)Example 4: Netflix Video Streaming
R — Requirements
Netflix เป็นเคสที่ bottleneck คือ "bandwidth" ล้วน ๆ ไม่ใช่ QPS — requirement สำคัญคือ stream ลื่นไม่กระตุก (adaptive bitrate), latency ต่ำทั่วโลก และรองรับหลายอุปกรณ์/คุณภาพ จุดนี้นำไปสู่การพึ่ง CDN เป็นหัวใจ:
Functional
- Upload + transcode video (content team)
- Browse catalog
- Stream video (adaptive bitrate!)
- Recommendations
- Multi-device sync (resume on phone)Non-Functional
- 230M subscribers
- 100M+ hours streamed daily
- Global (low latency everywhere)
- 99.99% availability
- Multiple device + quality
- Smooth playback (adaptive bitrate)Estimate
ตัวเลขที่ชี้ทุกการตัดสินใจของ Netflix คือ bandwidth ระดับ 25 Tbps ตอน peak — มากเกินกว่า data center ใด ๆ จะ serve ได้ นี่คือเหตุผลที่ Netflix ต้องสร้าง CDN ของตัวเอง (Open Connect) วางเซิร์ฟเวอร์ไว้ใกล้ผู้ใช้:
Concurrent streamers: ~5M (US peak)
Each: ~5 Mbps avg (HD)
Bandwidth: 5M × 5 Mbps = 25 Tbps !!!
→ Cannot serve from data center alone
→ Must use CDN at massive scaleA — Architecture
สถาปัตยกรรม Netflix แยกชัดเจนเป็นสองเส้นทาง — "data plane" (วิดีโอ) เสิร์ฟจาก CDN edge ที่ cache video chunk และ "control plane" (metadata, catalog, recommendation) ผ่าน API gateway ส่วน upload/transcode เป็นงาน batch ที่ทำไม่บ่อยแต่หนัก:
Key: Adaptive Bitrate Streaming (ABR)
หัวใจที่ทำให้วิดีโอเล่นลื่นบนทุก network คือ adaptive bitrate (ABR) — encode วิดีโอไว้หลายคุณภาพ ตัดเป็น chunk สั้น ๆ แล้วให้ client เลือกคุณภาพต่อ chunk ตามความเร็ว network ที่วัดได้ ทำให้ภาพไม่กระตุกแม้สัญญาณแกว่ง:
Video encoded at multiple qualities:
- 240p (low data)
- 480p
- 720p (HD)
- 1080p (FHD)
- 4K (UHD)
- HDR (premium)
Split into 10-second chunks each quality.
Client manifest:
- List of chunks at each quality level
Client decides per chunk:
- If network fast → high quality
- If slow → drop to lower quality
- Buffer fills → keep current
- Buffer drops → drop quality immediately
→ Smooth playback even with variable networkCDN Strategy — Netflix Open Connect ⭐
Netflix ไม่ได้ใช้ CDN เชิงพาณิชย์ แต่สร้าง "Open Connect" ของตัวเอง — วางเซิร์ฟเวอร์ไว้ใน ISP ใกล้ผู้ใช้ และ pre-load หนังที่คาดว่าจะฮิต (ทำนายด้วย ML) ลงไว้ตอนกลางคืน ผลคือ cache hit >95% สตรีมจากเครื่องใกล้บ้านแทบไม่ข้าม internet:
Netflix Open Connect:
- Custom CDN built by Netflix
- Servers in ISPs (ใกล้ user มากกว่า CDN ทั่วไป)
- Pre-load popular content overnight
- Cache hit rate: > 95%
Pre-positioning:
- Predict สิ่งที่ user จะดู (ML)
- Pre-load → local servers overnight
- Stream from local — almost zero internet hopRecommendations
ระบบแนะนำของ Netflix แบ่งเป็นสองส่วน — real-time (continue watching, trending ที่อัปเดตบ่อย) และ batch ML (personalized homepage, "because you watched X" คำนวณรายวัน) ส่วนนี้แสดงว่า recommendation ผสม pipeline หลายแบบและเครื่องมือ ML เข้ามาในระบบ:
Real-time:
- "Continue watching" → user's history (Cassandra)
- Top trending → updated hourly
Batch ML:
- "Because you watched X" → recompute daily
- Personalized homepage → per-user ML inference
- A/B test thumbnails
Tools:
- Spark for batch ML
- Kafka for event stream
- TensorFlow Serving for inference
- Vector DB (Pinecone, pgvector) สำหรับ similarityExample 5: WhatsApp / Chat App
R — Requirements
WhatsApp เป็นเคสที่ท้าทายด้วย "จำนวน connection ที่เปิดค้าง" ระดับร้อยล้านพร้อมกัน + ความต้องการ real-time + E2EE — requirement ที่ต้องชัดคือ delivery <100ms, รองรับ offline (queue ไว้ส่งทีหลัง) และไม่เก็บ message ระยะยาวเพราะเข้ารหัส:
Functional
- 1-1 messaging
- Group chat
- Online status / last seen
- Read receipts
- Send media (image, video, audio)
- End-to-end encryption (E2EE)Non-Functional
- 2B users globally
- 100B messages/day
- Real-time delivery (< 100ms)
- Offline support (messages queued)
- Mobile-first (low bandwidth)Estimate
ตัวเลขสำคัญของ WhatsApp คือ message ~1M/sec และ connection ค้างพร้อมกัน 100M+ — message เล็ก (~100 bytes) จึง bandwidth ไม่ใช่ปัญหา แต่การ "ถือ connection จำนวนมหาศาล" คือโจทย์หลักที่นำไปสู่ connection server แบบ stateful + sticky routing:
Messages: 100B/day = ~1M/sec avg, ~5M/sec peak
Avg msg: ~100 bytes (text)
Bandwidth: 5M × 100 = 500 MB/sec
Storage:
- Don't store messages long-term (E2EE)
- Server stores ~7 days then delete (or until delivered)
- Encrypted blob on device
Active connections: 100M+ at peak
- WebSocket / XMPP variant
- Multiple data centers, sticky routingA — Architecture
สถาปัตยกรรม WhatsApp มีหัวใจคือ connection server แบบ stateful (ถือ WebSocket ของแต่ละ user) + message queue ต่อ user สำหรับ offline delivery message routing จะหาว่า user ปลายทางต่ออยู่ server ไหนแล้ว push ผ่าน server นั้น (หรือ queue ไว้ถ้า offline):
Message Delivery
หัวใจของ chat app คือ "ส่ง message ให้ถึงปลายทาง" ซึ่งต่างกันตามว่าผู้รับ online หรือไม่ — ถ้า online ก็ route ผ่าน connection server แล้ว push ทันที (พร้อม ACK สำหรับ delivered/read), ถ้า offline ก็ queue ไว้ + push notification ปลุกเครื่อง แล้วส่งเมื่อกลับมาออนไลน์:
Bob Online
Alice → Bob (Bob online):
1. Alice's phone encrypts msg with Bob's public key
2. Sends to connection server
3. Server routes to Bob's connection server (via cluster pub-sub)
4. Bob's server delivers via WebSocket
5. Bob's phone decrypts → display
6. ACK sent back → Alice gets "delivered" checkmark
7. Bob reads → "read" ACK → AliceBob Offline
Alice → Bob (Bob offline):
1. Encrypt msg
2. Store in Bob's queue (encrypted)
3. Push notification ("New msg") → wake Bob's phone
4. Phone connects → pulls queued msgs
5. หลัง delivered, server deleteEnd-to-End Encryption (E2EE)
E2EE ทำให้แม้แต่เซิร์ฟเวอร์เองก็อ่าน message ไม่ได้ — เข้ารหัสที่เครื่องผู้ส่ง ถอดได้แค่ที่เครื่องผู้รับ เซิร์ฟเวอร์เป็นแค่ "ตัวกลางส่งของที่อ่านไม่ออก" ผลคือถ้าเซิร์ฟเวอร์ถูกแฮ็กก็ไม่มีข้อความให้อ่าน Signal Protocol คือมาตรฐานที่ WhatsApp/Signal ใช้:
Signal Protocol — ใช้ใน WhatsApp, Signal, Messenger (optional):
1. แต่ละ user generate key pair (private + public)
2. Public keys แลกกันผ่าน server (server ไม่เห็น private)
3. Each message:
- Generate per-message symmetric key
- Encrypt msg ด้วย symmetric key
- Encrypt symmetric key ด้วย recipient's public key
- ส่ง encrypted msg → server
- Server ไม่ decrypt ได้ (just routes)
4. Recipient:
- Decrypt symmetric key ด้วย private key
- Decrypt msg ด้วย symmetric key
Result:
- Server never sees plaintext
- Even if hacked → no historical messages readableGroup Chat — Sender Key
group chat ทำให้ E2EE ยากขึ้น — ถ้า encrypt แยกต่อสมาชิกแต่ละคน (100 คน = encrypt 100 รอบ) ผู้ส่งทำงานหนักเกินไป Sender Key แก้ด้วยการให้ group มี shared key ที่ encrypt ครั้งเดียวแล้วกระจาย แลกความซับซ้อนของ crypto กับ performance:
Group ของ 100 members:
Alice posts msg:
Naive approach:
1. Alice encrypt msg 100 times (once per member's key)
2. Send 100 encrypted copies to server
→ Alice ทำงาน 100x
Better: Sender Key (Signal protocol):
1. Group มี shared key (rotated regularly)
2. Alice encrypt once with shared key
3. Server distributes to members
4. Members decrypt ด้วย shared key
Trade-off: simpler crypto vs perfect forward secrecyPresence (Online Status)
Real-time online status:
- Every ~minute: each phone ส่ง "I'm here" heartbeat
- Server tracks: user_id → last_seen timestamp
- Friends subscribe to user's presence
- Server pushes status changes via WebSocketExpensive at scale — Facebook batches updates เพื่อ reduce load
Common Patterns ระหว่าง 5 Examples
ดูแล้วเห็น pattern ที่ใช้ซ้ำ ๆ:
1. CDN — static + popular content
2. Cache layer (Redis) — hot data
3. DB sharding — partition key
4. Async queue — non-critical work
5. WebSocket — real-time
6. Push notifications — offline users
7. ML batch — personalization
8. Multi-region — global users
9. Geographic sharding — data locality
10. Eventual consistency — non-critical pathsInterview gold: รู้ pattern ทั้ง 10 นี้ + apply ได้ → score ดี
Tips for Interview
1. Always start with clarifying questions
2. Estimate scale before designing
3. Draw high-level first, then detail
4. Discuss trade-offs explicitly
5. Identify bottleneck + address
6. Mention real-world references ("like Instagram's...")
7. Be okay with "I don't know" — propose investigation
8. Time-box yourself (don't get stuck on detail)Checkpoint — ออกแบบเอง
🛠️ Checkpoint 5.1 — Design Twitter from Scratch
Before reading Twitter example:
- Spend 60 min
- Apply RADIO framework
- Compare with example above
- จดสิ่งที่ "พลาด" หรือ "ดีกว่า"
🛠️ Checkpoint 5.2 — Design Instagram
Like Twitter but for photo:
- What's different?
- How to handle: image upload, multiple sizes, feed
🛠️ Checkpoint 5.3 — Design Slack/Discord
Real-time chat with:
- Channels (1-1000+ members)
- Search
- File upload
- Cross-device sync
🛠️ Checkpoint 5.4 — Design Spotify
Music streaming:
- 500M songs
- Recommendations
- Playlists
- Offline mode
🛠️ Checkpoint 5.5 — Design Reverse Image Search
Upload image → find similar:
- ML inference at scale
- Vector search (pgvector / Pinecone)
- Image indexing
สรุปบท
ทบทวนสิ่งที่ได้จากการออกแบบ 5 ระบบจริง — ทุกเคสใช้ process เดียวกัน (RADIO) และ pattern ร่วมกัน (CDN, cache, sharding, async) แต่มี "ความท้าทายเฉพาะ" ของตัวเอง (Twitter=hybrid feed, Uber=geo matching, Netflix=CDN+ABR, WhatsApp=E2EE+connection) เช็กลิสต์นี้สรุปแก่นแต่ละเคส:
✅ Practice = key — design 5 systems ก่อน interview
✅ Process: Requirements → Estimate → High-level → Deep dive → Bottleneck
✅ Common patterns: CDN, cache, sharding, async, WebSocket
✅ Trade-offs: sync vs async, push vs pull, strong vs eventual
✅ Twitter uses hybrid push/pull for celebrity issue
✅ Uber uses geo-hash (H3) + Redis Geo for matching
✅ Netflix uses Open Connect CDN + adaptive bitrate + ML rec
✅ WhatsApp uses E2EE + sender key + per-user queue + push notification
✅ Each system has unique challenges + general patterns + specific tricks
7. Bonus — Other Common Interview Prompts
ตอนนี้คุณเข้าใจ RADIO framework และทำ 5 examples แล้ว — นี่คือ "skeleton" สำหรับ 3 prompts ที่เจอบ่อยใน interview แต่ใน workbook นี้ไม่ลึกเต็ม ลองคิดตามแล้ว fill in รายละเอียดเอง:
Example 6 (skeleton): Newsfeed (Instagram / Facebook)
text
Requirements:
- 1B users, 500M DAU
- Post: text + image + video
- Feed: posts of people you follow, ranked
- Read/write: 100:1
Key design:
- Hybrid fanout (similar to Twitter)
- Normal users: push to followers' feed (Redis)
- Top celebrities (>1M followers): pull at read time
- Ranking: ML model (CTR prediction)
- Features: recency, engagement, friend-of-friend
- Media: CDN + transcoding pipeline (S3 + Kafka + workers)
- Storage:
- Posts: Cassandra (write-heavy timeline)
- Graph (follow): Redis sorted set or graph DB
- Media metadata: PostgresExample 7 (skeleton): Stripe Payment
text
Requirements:
- 10K req/sec sustained, 50K peak (Black Friday)
- p99 < 500ms
- 99.999% availability
- Strong consistency (no double-charge)
- Compliance: PCI DSS
Key design:
- API Gateway with idempotency key (Stripe-style)
- Idempotency key in DB + TTL
- Atomic check-and-set: if exists return cached, else execute + cache
- Payment service:
- 2-phase commit with card network (auth → capture)
- Saga for refund flow
- Storage:
- Postgres (ACID, RBAC, encryption at rest)
- Sharded by merchant_id
- Reliability:
- Multi-region active-active (Spanner-style consistency)
- Circuit breaker for card network (Visa, Mastercard)
- Rate limit per merchant + per card
Key patterns:
- Idempotency keys (RFC 9586 standard)
- Webhooks with HMAC signing (at-least-once + idempotent consumer)
- Async refunds via SagaExample 8 (skeleton): Search (Google-scale)
text
Requirements:
- 100K QPS, 10B documents
- p99 < 200ms
- Real-time indexing (< 1 min lag)
Key design:
- Indexing pipeline:
- Crawler → Parser → Tokenizer → Inverted index
- Kafka + workers + Elasticsearch / Vespa
- Serving:
- Sharded inverted index (by term hash)
- Each query → fan-out to all shards → merge top-K
- Cache popular queries (1% queries = 99% volume)
- Ranking:
- Multi-stage: candidate generation → ranking → re-ranking
- ML models per stage (BM25 → BERT → personalization)
- Auto-suggest: Trie + popularity score
Key patterns:
- Scatter-gather (fan-out / merge top-K)
- Multi-tier cache (L1 = top queries, L2 = warm results)
- Real-time indexing via Kafka + segment-based mergingคำศัพท์เพิ่ม
| คำศัพท์ | ความหมาย |
|---|---|
| Fanout-on-write | Pre-compute feeds when post |
| Fanout-on-read | Compute feed at read time |
| Hybrid fanout | Push for normal users, pull for celebrities |
| Base62 | Encoding using 0-9, a-z, A-Z (62 chars) |
| Snowflake ID | Twitter's distributed ID (timestamp + worker + seq) |
| ULID | Sortable UUID alternative |
| Geohash | Encode lat/lng as string |
| H3 | Uber's hexagonal grid system |
| S2 | Google's spherical geo index |
| Adaptive bitrate (ABR) | Streaming quality adjusts to network |
| HLS / DASH | Adaptive streaming protocols |
| Open Connect | Netflix's custom CDN in ISPs |
| E2EE | End-to-End Encryption |
| Signal Protocol | E2EE protocol used in WhatsApp, Signal |
| Sender Key | Shared group key (efficient group encryption) |
| APNS / FCM | Push notification services (Apple / Google) |
| Presence | Online/offline status tracking |
| WebSocket sticky routing | Connection อยู่ server เดิม |