Skip to content

บทที่ 5 — Worked Examples

← บทที่ 4 | สารบัญ | บทที่ 6 →


ก่อนอ่าน — ต้องรู้อะไรมาก่อน?

บทนี้รวมทุก 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

ขั้นตอน:

  1. อ่าน "Requirements" + "Non-Functional"
  2. ปิดเฉลย — set timer 30-45 นาที — ลองออกแบบเอง
  3. เปิดอ่านเฉลย — เปรียบเทียบ
  4. จดสิ่งที่ "พลาด" หรือ "ใหม่"

🟢 มือใหม่: ครั้งแรกอ่านเฉลยก่อนแล้วทำความเข้าใจ 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) Expiration

Non-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) และความสั้น/เดายาก ส่วนนี้เทียบทั้งสามเพื่อเลือกให้เหมาะ:

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 สำหรับคนสนใจคณิตศาสตร์ มือใหม่อ่านแค่ตารางผลลัพธ์ก็พอ)

Space50% 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-tripID หายเมื่อ service crash (ไม่ค่อย contiguous) · ยัง predictable
Content-hash + collision suffixcode = 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.4301 Moved Permanently บอก client ว่า "ลิงก์นี้ย้ายถาวร เก็บ cache ได้ยาว ๆ" → browser และ proxy จะ cache อย่างก้าวร้าว (บางตัว cache แบบ "ตลอดไป" จน user ล้าง history) → ครั้งต่อ ๆ ไปที่ user คลิก short URL เดิม จะข้าม server ของเราเลย

ผลคือ:

Status codeBrowser cachingAnalytics accuracyเหมาะกับ
301 Moved Permanentlyaggressive (อาจถาวร)นับคลิกขาด — ครั้งแรกเท่านั้น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 distributed

4. CDN for Long-tail

Most URLs accessed 1-2 times
แต่ ~1% URLs = 90% of traffic (heavy tail)

CDN caches popular URLs → ~80% redirects ไม่ hit app

Bottleneck + Scale Plan

ส่วนนี้สรุปว่าระบบจะเจอคอขวดตรงไหนเมื่อ scale และแก้ยังไง — ตารางจับคู่ bottleneck (DB write, hot read, analytics write) กับวิธีแก้ ทำให้เห็นภาพว่าควรลงทุนเรื่องไหนก่อนเมื่อ traffic โต:

BottleneckSolution
DB write (create URL)Sharding by code
Hot URL readsCDN + Redis cache
Click analytics writesAsync 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
- Search

Non-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: sharded

A — 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-heavy

Approach 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 check

Approach 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 followers

Insight ที่ทำให้ 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 suggestions

Sharding

ข้อมูลของ 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
- Rating

Non-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 cache

A — 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 nearest

Redis 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 radius

Real-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 scale

A — 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 network

CDN 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 hop

Recommendations

ระบบแนะนำของ 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) สำหรับ similarity

Example 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 routing

A — 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 → Alice

Bob 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 delete

End-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 readable

Group 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 secrecy

Presence (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 WebSocket

Expensive 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 paths

Interview 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: Postgres

Example 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 Saga

Example 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-writePre-compute feeds when post
Fanout-on-readCompute feed at read time
Hybrid fanoutPush for normal users, pull for celebrities
Base62Encoding using 0-9, a-z, A-Z (62 chars)
Snowflake IDTwitter's distributed ID (timestamp + worker + seq)
ULIDSortable UUID alternative
GeohashEncode lat/lng as string
H3Uber's hexagonal grid system
S2Google's spherical geo index
Adaptive bitrate (ABR)Streaming quality adjusts to network
HLS / DASHAdaptive streaming protocols
Open ConnectNetflix's custom CDN in ISPs
E2EEEnd-to-End Encryption
Signal ProtocolE2EE protocol used in WhatsApp, Signal
Sender KeyShared group key (efficient group encryption)
APNS / FCMPush notification services (Apple / Google)
PresenceOnline/offline status tracking
WebSocket sticky routingConnection อยู่ server เดิม

← บทที่ 4 | บทที่ 6 → Interview Strategy