System Design เป็นทักษะที่สำคัญที่สุดสำหรับ Senior Developer และ Software Architect ไม่ว่าจะเป็นการออกแบบระบบจริงในที่ทำงานหรือการสัมภาษณ์งานในบริษัท Tech ชั้นนำ คุณจะต้องเข้าใจวิธีการออกแบบระบบที่รองรับผู้ใช้หลายล้านคน มีความเสถียร ปลอดภัย และขยายตัวได้
บทความนี้จะสอน System Design ตั้งแต่แนวคิดพื้นฐานจนถึงการออกแบบระบบจริง ครอบคลุม Scalability, Load Balancing, Caching, Database Design, CAP Theorem, Message Queues และตัวอย่างการออกแบบระบบยอดนิยม เช่น URL Shortener, Chat System และ Newsfeed
ทำไม System Design ถึงสำคัญ?
ในยุคที่แอปพลิเคชันต้องรองรับผู้ใช้หลายล้านคนพร้อมกัน การเขียนโค้ดที่ทำงานได้อย่างเดียวไม่เพียงพอ คุณต้องคิดเรื่อง Performance ว่าระบบตอบสนองเร็วแค่ไหน Reliability ว่าระบบพังไหมเมื่อมีปัญหา Scalability ว่าระบบรองรับ Traffic ที่เพิ่มขึ้นได้หรือไม่ และ Maintainability ว่าระบบดูแลรักษาง่ายไหมเมื่อทีมโตขึ้น
ในการสัมภาษณ์งานตำแหน่ง Senior Developer ที่บริษัท Tech ใหญ่ๆ เช่น Google, Meta, Amazon, LINE, Agoda รอบ System Design มักจะเป็นรอบที่ตัดสินว่าคุณจะได้ตำแหน่ง Senior หรือไม่ เพราะมันแสดงให้เห็นว่าคุณมองปัญหาในภาพรวม ไม่ใช่แค่เขียนโค้ดฟังก์ชันเดียว
Scalability — การขยายตัวของระบบ
Scalability คือความสามารถของระบบในการรองรับ Load ที่เพิ่มขึ้น ไม่ว่าจะเป็นจำนวนผู้ใช้ จำนวน Request หรือปริมาณข้อมูลที่เพิ่มขึ้น มีสองแนวทางหลักในการ Scale ระบบ
Vertical Scaling (Scale Up)
เพิ่มพลังให้เครื่องเดิม เช่น เพิ่ม CPU, RAM, SSD ข้อดีคือง่าย ไม่ต้องเปลี่ยนโค้ด แต่มีข้อจำกัดคือมีเพดานสูงสุดที่เครื่องเดียวจะรับได้ และถ้าเครื่องพังก็คือทั้งระบบล่ม (Single Point of Failure)
Horizontal Scaling (Scale Out)
เพิ่มจำนวนเครื่องแทน โดยกระจาย Load ไปยังหลายเครื่อง ข้อดีคือไม่มีเพดาน เพิ่มเครื่องได้เรื่อยๆ และถ้าเครื่องหนึ่งพังก็เครื่องอื่นรับแทนได้ แต่ต้องออกแบบให้ Application เป็น Stateless และจัดการเรื่อง Data Consistency ให้ดี
| หัวข้อ | Vertical Scaling | Horizontal Scaling |
|---|---|---|
| วิธีการ | เพิ่ม CPU/RAM เครื่องเดิม | เพิ่มจำนวนเครื่อง |
| ความซับซ้อน | ต่ำ | สูง |
| ค่าใช้จ่าย | เพิ่มแบบ Exponential | เพิ่มแบบ Linear |
| เพดาน | จำกัด (Hardware Limit) | ไม่จำกัด (เพิ่มเครื่องได้เรื่อยๆ) |
| Availability | SPOF (Single Point of Failure) | High Availability (HA) |
| ตัวอย่าง | AWS RDS Scale Up | เพิ่ม EC2 ใน Auto Scaling Group |
Load Balancing — กระจาย Traffic
Load Balancer เป็นตัวกลางที่รับ Request จากผู้ใช้และกระจายไปยัง Server หลายตัว ทำให้ไม่มี Server ตัวไหนรับ Load หนักเกินไป และยังช่วยเรื่อง High Availability เมื่อ Server ตัวใดตัวหนึ่งพัง Load Balancer จะหยุดส่ง Traffic ไปที่ Server นั้นอัตโนมัติ
อัลกอริทึมการกระจาย Load
| Algorithm | วิธีการ | เหมาะกับ |
|---|---|---|
| Round Robin | วนส่งทีละ Server ตามลำดับ | Server ที่มี Spec เท่ากัน |
| Weighted Round Robin | Server แรงกว่าได้ส่วนแบ่งมากกว่า | Server ที่มี Spec ต่างกัน |
| Least Connections | ส่งไป Server ที่มี Connection น้อยสุด | Request ที่ใช้เวลาต่างกัน |
| IP Hash | Hash IP ของผู้ใช้เพื่อส่งไป Server เดิมเสมอ | Session Sticky |
| Least Response Time | ส่งไป Server ที่ตอบเร็วที่สุด | ต้องการ Performance สูงสุด |
ระดับของ Load Balancing
- Layer 4 (Transport): กระจายตาม IP + Port รวดเร็วมากเพราะไม่ต้องดูเนื้อหา Packet ตัวอย่างเช่น AWS NLB, HAProxy TCP mode
- Layer 7 (Application): กระจายตามเนื้อหา HTTP เช่น URL Path, Header, Cookie ยืดหยุ่นกว่าแต่ช้ากว่า Layer 4 เล็กน้อย ตัวอย่างเช่น AWS ALB, Nginx, HAProxy HTTP mode
# Nginx Load Balancer Configuration
upstream backend {
# Weighted Round Robin
server app1.internal:8080 weight=3;
server app2.internal:8080 weight=2;
server app3.internal:8080 weight=1;
# Health Check
# Nginx จะหยุดส่ง Traffic ไปที่ Server ที่ fail
# max_fails=3 ลองผิด 3 ครั้ง
# fail_timeout=30s หยุดส่ง 30 วินาทีก่อนลองใหม่
server app1.internal:8080 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
# Connection pooling
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeout settings
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Route API ไป API servers
location /api/ {
proxy_pass http://api_backend;
}
# Route static files ไป CDN/Static servers
location /static/ {
proxy_pass http://static_backend;
proxy_cache_valid 200 1d;
}
}
Caching — เร่งความเร็วระบบ
Caching เป็นเทคนิคที่ทรงพลังที่สุดในการเพิ่ม Performance ของระบบ หลักการคือเก็บผลลัพธ์ที่คำนวณหรือ Query มาแล้วไว้ในหน่วยความจำที่เข้าถึงได้เร็ว เมื่อมี Request เดิมเข้ามาก็ส่งผลลัพธ์จาก Cache ไปเลยโดยไม่ต้องคำนวณใหม่ ช่วยลด Latency และลด Load บน Database ได้อย่างมาก
ระดับของ Cache
| ระดับ | ตำแหน่ง | ตัวอย่าง | Latency |
|---|---|---|---|
| Browser Cache | เบราว์เซอร์ผู้ใช้ | Cache-Control Headers | 0ms |
| CDN Cache | Edge Server ทั่วโลก | CloudFlare, AWS CloudFront | 10-50ms |
| Application Cache | Application Server | Redis, Memcached | 1-5ms |
| Database Cache | Database Server | Query Cache, Buffer Pool | 5-10ms |
Cache Strategies
# Cache-Aside (Lazy Loading) — กลยุทธ์ที่นิยมมากที่สุด
import redis
import json
cache = redis.Redis(host='localhost', port=6379, db=0)
def get_user(user_id):
# 1. ลองดึงจาก Cache ก่อน
cache_key = f"user:{user_id}"
cached = cache.get(cache_key)
if cached:
return json.loads(cached) # Cache Hit!
# 2. ถ้าไม่มีใน Cache ดึงจาก Database
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. เก็บผลลัพธ์ลง Cache (TTL 1 ชั่วโมง)
cache.setex(cache_key, 3600, json.dumps(user))
return user # Cache Miss
def update_user(user_id, data):
# อัปเดต Database
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
# ลบ Cache เก่าออก (Invalidation)
cache.delete(f"user:{user_id}")
# Write-Through — เขียน Cache และ Database พร้อมกัน
def save_user_write_through(user_id, data):
# เขียนทั้ง Cache และ DB
cache.setex(f"user:{user_id}", 3600, json.dumps(data))
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
# ข้อดี: Cache กับ DB ตรงกันเสมอ
# ข้อเสีย: Write ช้าลง (ต้องเขียน 2 ที่)
# Write-Behind (Write-Back) — เขียน Cache ก่อน แล้ว Async เขียน DB ทีหลัง
def save_user_write_behind(user_id, data):
# เขียน Cache ทันที
cache.setex(f"user:{user_id}", 3600, json.dumps(data))
# ส่งไป Background Queue เพื่อเขียน DB
queue.enqueue('write_db', user_id=user_id, data=data)
# ข้อดี: Write เร็วมาก
# ข้อเสีย: ถ้า Cache พังก่อน DB เขียน ข้อมูลหาย
Cache Invalidation — ปัญหาที่ยากที่สุด
มีคำกล่าวว่า "There are only two hard things in Computer Science: cache invalidation and naming things" การตัดสินใจว่าเมื่อไหร่ควรลบ Cache และวิธีจัดการเป็นเรื่องที่ต้องคิดให้รอบคอบ
- TTL (Time to Live): กำหนดอายุ Cache เช่น 5 นาที 1 ชั่วโมง เมื่อหมดอายุ Cache จะถูกลบอัตโนมัติ ง่ายสุดแต่ข้อมูลอาจเก่า (Stale Data) ช่วง TTL ยังไม่หมด
- Event-based Invalidation: ลบ Cache เมื่อข้อมูลถูกเปลี่ยนแปลง เช่น เมื่อ Update/Delete ข้อมูล ให้ลบ Cache ที่เกี่ยวข้องทันที ข้อมูลใหม่กว่าแต่ต้อง Track ว่า Cache ไหนเกี่ยวข้องกับข้อมูลไหน
- Version-based: เพิ่ม Version Number ใน Cache Key เมื่อข้อมูลเปลี่ยน ให้เพิ่ม Version ทำให้ Cache Key เปลี่ยนโดยอัตโนมัติ
Database Design — การออกแบบฐานข้อมูล
SQL vs NoSQL
| หัวข้อ | SQL (Relational) | NoSQL |
|---|---|---|
| โครงสร้าง | Schema ตายตัว (Table, Row, Column) | ยืดหยุ่น (Document, Key-Value, Graph) |
| ความสัมพันธ์ | JOIN ได้ง่าย | ต้อง Denormalize หรือ Query หลายครั้ง |
| ACID | รองรับเต็มรูปแบบ | บางตัวรองรับบางส่วน (BASE) |
| Scaling | Vertical เป็นหลัก (Read Replica สำหรับ Read) | Horizontal Scaling ง่าย (Sharding) |
| เหมาะกับ | ข้อมูลที่มีความสัมพันธ์ซับซ้อน ต้องการ Consistency | ข้อมูลมหาศาล ต้องการ Flexibility และ Speed |
| ตัวอย่าง | PostgreSQL, MySQL | MongoDB, DynamoDB, Cassandra, Redis |
Database Replication
Replication คือการทำสำเนาข้อมูลไปยังหลาย Server เพื่อเพิ่ม Availability และ Read Performance รูปแบบที่พบบ่อยที่สุดคือ Master-Slave (Primary-Replica) โดย Master รับ Write ทั้งหมด แล้ว Replicate ข้อมูลไปยัง Slave หลายตัว Slave รับเฉพาะ Read
# Database Replication Architecture
#
# [Client] --Write--> [Master DB]
# |
# Replication (Async/Semi-sync)
# / | \
# [Slave 1] [Slave 2] [Slave 3]
# ^ ^ ^
# | | |
# [Client Read Requests via Load Balancer]
# Python — Read/Write Splitting
class DatabaseRouter:
def __init__(self):
self.master = create_connection("master-db.internal:5432")
self.replicas = [
create_connection("replica1-db.internal:5432"),
create_connection("replica2-db.internal:5432"),
create_connection("replica3-db.internal:5432"),
]
self._replica_index = 0
def get_write_connection(self):
return self.master
def get_read_connection(self):
# Round Robin เลือก Replica
conn = self.replicas[self._replica_index]
self._replica_index = (self._replica_index + 1) % len(self.replicas)
return conn
def execute_write(self, query, params=None):
conn = self.get_write_connection()
return conn.execute(query, params)
def execute_read(self, query, params=None):
conn = self.get_read_connection()
return conn.execute(query, params)
Database Sharding
Sharding คือการแบ่งข้อมูลออกเป็นส่วนๆ (Shards) แล้วเก็บไว้คนละ Server ใช้เมื่อข้อมูลมีขนาดใหญ่มากจนเครื่องเดียวรับไม่ไหว เป็นเทคนิค Horizontal Scaling ของ Database
# Sharding Strategies:
#
# 1. Range-based Sharding — แบ่งตามช่วง
# User ID 1-1M -> Shard 1
# User ID 1M-2M -> Shard 2
# ข้อเสีย: Hotspot (Shard ใหม่จะโดน Load มากกว่า)
#
# 2. Hash-based Sharding — Hash แล้วแบ่ง
# shard = hash(user_id) % num_shards
# ข้อดี: กระจายสม่ำเสมอ
# ข้อเสีย: เพิ่ม Shard ยาก (ต้อง Resharding)
#
# 3. Directory-based Sharding — Lookup Table
# เก็บ Mapping ว่า Key ไหนอยู่ Shard ไหน
# ข้อดี: ยืดหยุ่นสูง
# ข้อเสีย: Lookup Table เป็น SPOF
# Consistent Hashing — แก้ปัญหา Resharding
import hashlib
class ConsistentHash:
def __init__(self, nodes, virtual_nodes=150):
self.ring = {}
self.sorted_keys = []
for node in nodes:
for i in range(virtual_nodes):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
self.sorted_keys.append(key)
self.sorted_keys.sort()
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def get_node(self, data_key):
if not self.ring:
return None
h = self._hash(str(data_key))
for key in self.sorted_keys:
if h <= key:
return self.ring[key]
return self.ring[self.sorted_keys[0]]
# ใช้งาน
shards = ConsistentHash(["shard1", "shard2", "shard3"])
target = shards.get_node("user:12345") # -> "shard2"
CAP Theorem
CAP Theorem ระบุว่าระบบ Distributed Database ไม่สามารถมีทั้ง 3 อย่างพร้อมกันได้ ต้องเลือกเพียง 2 จาก 3
- Consistency (C): ทุก Node เห็นข้อมูลเหมือนกันในเวลาเดียวกัน เมื่อเขียนสำเร็จ ทุก Read หลังจากนั้นจะได้ข้อมูลใหม่
- Availability (A): ทุก Request ได้รับ Response เสมอ แม้บาง Node จะพังก็ยังใช้งานได้
- Partition Tolerance (P): ระบบยังทำงานได้แม้ Network ระหว่าง Node จะขาด
ในความเป็นจริง Network Partition เกิดขึ้นได้เสมอ ดังนั้น P เป็นสิ่งที่ต้องมี เหลือให้เลือกระหว่าง C กับ A:
| เลือก | ได้ | เสีย | ตัวอย่าง |
|---|---|---|---|
| CP | Consistency + Partition Tolerance | ระบบอาจ Unavailable ชั่วขณะ | MongoDB (default), HBase, Redis Cluster |
| AP | Availability + Partition Tolerance | ข้อมูลอาจไม่ตรงกันชั่วขณะ | Cassandra, DynamoDB, CouchDB |
Consistency Patterns
- Strong Consistency: อ่านแล้วได้ข้อมูลล่าสุดเสมอ ช้ากว่าเพราะต้องรอ Consensus ระหว่าง Node ตัวอย่าง RDBMS ปกติ ใช้กับระบบการเงินที่ยอมผิดพลาดไม่ได้
- Eventual Consistency: ข้อมูลจะตรงกันในที่สุด แต่อาจมีช่วงที่ Node ต่างๆ เห็นข้อมูลไม่ตรงกัน เร็วกว่ามาก เหมาะกับ Social Media, CDN ที่ยอมให้ข้อมูลเก่าได้ชั่วคราว
- Read-your-writes Consistency: ผู้ใช้ที่เขียนข้อมูลจะอ่านเห็นข้อมูลใหม่ทันที แต่ผู้ใช้คนอื่นอาจเห็นข้อมูลเก่าอยู่ชั่วคราว เป็นจุดกึ่งกลางที่ใช้กันมากในทางปฏิบัติ
Message Queues — การสื่อสารแบบ Asynchronous
Message Queue เป็นตัวกลางที่ช่วยให้ Service ต่างๆ สื่อสารกันแบบ Asynchronous ช่วยลด Coupling ระหว่าง Service เพิ่มความเสถียร และรองรับ Spike Traffic ได้ดีขึ้น
# ตัวอย่างระบบ Order Processing ด้วย Message Queue
#
# [User] -> [Order Service] -> [Message Queue] -> [Payment Service]
# | -> [Inventory Service]
# | -> [Notification Service]
# | -> [Analytics Service]
#
# ข้อดี:
# 1. ถ้า Payment Service พังชั่วคราว Message ไม่หาย (Durability)
# 2. Order Service ไม่ต้องรอ Payment Service ทำเสร็จ (Async)
# 3. เพิ่ม Consumer ได้ตามต้องการ (Scalability)
# 4. Service ไม่ต้องรู้จักกัน (Decoupling)
# Python — Producer (ส่ง Message)
import pika
import json
connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq'))
channel = connection.channel()
channel.queue_declare(queue='orders', durable=True)
def publish_order(order):
channel.basic_publish(
exchange='',
routing_key='orders',
body=json.dumps(order),
properties=pika.BasicProperties(
delivery_mode=2, # Message จะถูกเก็บบน Disk (Persistent)
content_type='application/json'
)
)
# Python — Consumer (รับ Message)
def process_order(ch, method, properties, body):
order = json.loads(body)
try:
process_payment(order)
update_inventory(order)
send_notification(order)
ch.basic_ack(delivery_tag=method.delivery_tag) # ยืนยันว่าทำเสร็จแล้ว
except Exception as e:
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True) # ส่งกลับ Queue
channel.basic_qos(prefetch_count=1) # รับทีละ 1 Message
channel.basic_consume(queue='orders', on_message_callback=process_order)
Microservices vs Monolith
Monolithic Architecture
ทุกอย่างอยู่ใน Application เดียว ง่ายต่อการพัฒนาและ Deploy ในช่วงแรก แต่เมื่อ Codebase โตขึ้น จะเริ่มยากต่อการดูแลรักษา ทดสอบ และ Deploy เพราะการเปลี่ยนแปลงเล็กน้อยอาจกระทบทั้งระบบ การ Scale ต้อง Scale ทั้ง Application แม้มีแค่ส่วนเดียวที่โดน Load มาก
Microservices Architecture
แยกระบบออกเป็น Service เล็กๆ หลายตัว แต่ละตัวทำหน้าที่เฉพาะ สื่อสารกันผ่าน API หรือ Message Queue ข้อดีคือ Deploy แต่ละ Service ได้อิสระ Scale เฉพาะ Service ที่ต้องการ ใช้ Technology Stack ที่เหมาะสมกับแต่ละ Service ได้ แต่ข้อเสียคือเพิ่มความซับซ้อนของระบบอย่างมาก ต้องจัดการเรื่อง Service Discovery, Distributed Tracing, Data Consistency ข้าม Service
| หัวข้อ | Monolith | Microservices |
|---|---|---|
| Deployment | Deploy ทั้งก้อน | Deploy แยก Service |
| Scaling | Scale ทั้ง App | Scale เฉพาะ Service |
| Development | ง่ายตอนแรก ยากตอนหลัง | ยากตอนแรก ง่ายตอนหลัง |
| Debugging | ง่าย (ทุกอย่างอยู่ที่เดียว) | ยาก (Distributed Tracing) |
| Tech Stack | เดียวกันทั้งหมด | เลือกได้ตาม Service |
| Team | ทีมเดียวดูแลทั้งหมด | แต่ละทีมดูแลแต่ละ Service |
Rate Limiting — ควบคุมปริมาณ Request
Rate Limiting เป็นเทคนิคป้องกันไม่ให้ผู้ใช้หรือ Client ส่ง Request มากเกินไป ช่วยป้องกัน DDoS Attack ป้องกัน API Abuse และทำให้ระบบมีเสถียรภาพ
# Token Bucket Algorithm — อัลกอริทึม Rate Limiting ที่นิยมที่สุด
import time
import redis
class TokenBucket:
def __init__(self, redis_client, key, max_tokens, refill_rate):
self.redis = redis_client
self.key = key
self.max_tokens = max_tokens # จำนวน Token สูงสุดในถัง
self.refill_rate = refill_rate # Token ที่เติมต่อวินาที
def allow_request(self):
pipe = self.redis.pipeline()
now = time.time()
# ดึงข้อมูลปัจจุบัน
data = self.redis.hgetall(self.key)
tokens = float(data.get('tokens', self.max_tokens))
last_refill = float(data.get('last_refill', now))
# เติม Token ตามเวลาที่ผ่านไป
elapsed = now - last_refill
tokens = min(self.max_tokens, tokens + elapsed * self.refill_rate)
if tokens >= 1:
# อนุญาต — ใช้ Token 1 อัน
tokens -= 1
pipe.hset(self.key, mapping={'tokens': tokens, 'last_refill': now})
pipe.expire(self.key, 3600)
pipe.execute()
return True
else:
# ปฏิเสธ — Token หมด
pipe.hset(self.key, mapping={'tokens': tokens, 'last_refill': now})
pipe.execute()
return False
# ใช้งาน: 100 requests / minute per user
limiter = TokenBucket(redis_client, f"rate:{user_id}", max_tokens=100, refill_rate=100/60)
ตัวอย่างการออกแบบระบบ (System Design Examples)
ออกแบบ URL Shortener (เช่น bit.ly)
# Requirements:
# - ผู้ใช้ส่ง Long URL -> ระบบสร้าง Short URL (7 ตัวอักษร)
# - เมื่อเข้า Short URL -> Redirect ไป Long URL
# - รองรับ 100M URLs, 10K writes/sec, 100K reads/sec
# Architecture:
# [Client] -> [Load Balancer] -> [API Servers (Stateless)]
# |
# [Redis Cache] + [PostgreSQL/Cassandra]
#
# Key Decisions:
# 1. Short URL Generation: Base62 encoding ของ Auto-increment ID
# หรือ MD5 Hash ตัดเอา 7 ตัวแรก (ต้องเช็ค Collision)
# 2. Database: NoSQL (Cassandra) สำหรับ Scale, SQL สำหรับเริ่มต้น
# 3. Cache: เก็บ Short->Long mapping ใน Redis (Cache-Aside)
# 4. Analytics: แยก Service สำหรับนับ Click (Async via Queue)
import hashlib
import string
BASE62 = string.ascii_letters + string.digits # a-z A-Z 0-9
def encode_base62(num):
if num == 0:
return BASE62[0]
result = []
while num:
result.append(BASE62[num % 62])
num //= 62
return ''.join(reversed(result))
def generate_short_url(long_url, counter):
# วิธี 1: Base62 ของ Counter (ไม่มี Collision)
return encode_base62(counter)
# วิธี 2: Hash-based (ต้องเช็ค Collision)
# h = hashlib.md5(long_url.encode()).hexdigest()
# return encode_base62(int(h[:10], 16))[:7]
ออกแบบ Chat System (เช่น LINE, WhatsApp)
# Requirements:
# - 1-on-1 Chat และ Group Chat
# - รองรับ 50M DAU, 40 messages/user/day
# - ส่ง/รับข้อความ Real-time
# - เก็บประวัติ Chat, Read Receipt, Online Status
# Architecture:
# [Mobile/Web Client] <--WebSocket--> [Chat Servers (Stateful)]
# |
# [Message Queue]
# / \
# [Message Store] [Push Notification]
# (Cassandra) (FCM/APNs)
#
# Key Decisions:
# 1. Protocol: WebSocket สำหรับ Real-time bidirectional
# 2. Message Store: Cassandra (Partition by chat_id, sort by timestamp)
# 3. Service Discovery: ใช้ Redis เก็บ User -> Chat Server mapping
# 4. Group Chat: Fan-out on write (เขียน message ไปทุก member's inbox)
# หรือ Fan-out on read (member ดึง message จาก group inbox)
# Message Schema (Cassandra)
# CREATE TABLE messages (
# chat_id UUID,
# message_id TIMEUUID,
# sender_id UUID,
# content TEXT,
# type TEXT, -- 'text', 'image', 'video'
# created_at TIMESTAMP,
# PRIMARY KEY (chat_id, message_id)
# ) WITH CLUSTERING ORDER BY (message_id DESC);
ออกแบบ Newsfeed (เช่น Facebook, Twitter)
# Requirements:
# - ผู้ใช้เห็น Post จากเพื่อนและ Pages ที่ Follow
# - เรียงตาม Relevance + Time
# - รองรับ 300M DAU
# Architecture:
# [Client] -> [Load Balancer] -> [Feed Service]
# |
# [Feed Cache (Redis)] <- [Feed Generator]
# |
# [Post Service] + [Social Graph] + [Ranking Service]
#
# Two Approaches:
#
# 1. Fan-out on Write (Push Model) — เมื่อ User A โพสต์
# เขียน Post ไปทุก Follower's Feed Cache ทันที
# ข้อดี: อ่าน Feed เร็วมาก (อ่านจาก Cache)
# ข้อเสีย: Write แพงมาก ถ้ามี Follower ล้านคน = เขียนล้านครั้ง
# เหมาะกับ: User ที่มี Follower ไม่มาก
#
# 2. Fan-out on Read (Pull Model) — เมื่ออ่าน Feed
# ดึง Post ล่าสุดจากทุกคนที่ Follow แล้ว Merge + Rank
# ข้อดี: Write ง่าย (เขียนที่เดียว)
# ข้อเสีย: อ่าน Feed ช้า (ต้อง Query หลาย Source)
# เหมาะกับ: Celebrity/Pages ที่มี Follower เยอะมาก
#
# Hybrid Approach (ที่ Facebook/Twitter ใช้):
# - User ทั่วไป (Follower < 10K): Fan-out on Write
# - Celebrity (Follower > 10K): Fan-out on Read
# - Merge ทั้งสอง Source เมื่อสร้าง Feed
Back-of-Envelope Estimation — การประมาณอย่างรวดเร็ว
ในการสัมภาษณ์ System Design คุณต้องประมาณตัวเลขอย่างรวดเร็ว นี่คือตัวเลขที่ควรจำ
| Resource | Latency / Throughput |
|---|---|
| L1 Cache Reference | 0.5 ns |
| L2 Cache Reference | 7 ns |
| Main Memory Reference | 100 ns |
| SSD Random Read | 150 microsec |
| HDD Seek | 10 ms |
| Network Round Trip (Same Datacenter) | 0.5 ms |
| Network Round Trip (Cross-continent) | 150 ms |
| Read 1 MB from SSD | 1 ms |
| Read 1 MB from Network | 10 ms |
| Read 1 MB from HDD | 20 ms |
# Quick Math:
# - 1 วันมี 86,400 วินาที (ประมาณ 100K)
# - 1 เดือนมี 2.5 ล้านวินาที
# - QPS (Queries Per Second) = DAU * queries_per_user / 86400
# - Storage = DAU * data_per_user * retention_days
#
# ตัวอย่าง: Twitter-like System
# - 300M DAU, 2 tweets/user/day, 280 bytes/tweet
# - Write QPS = 300M * 2 / 86400 = ~7000 QPS
# - Peak QPS = 7000 * 3 = ~21000 QPS (3x average)
# - Daily Storage = 300M * 2 * 280 bytes = ~168 GB/day
# - Yearly Storage = 168 * 365 = ~60 TB/year (ไม่รวม Media)
RESHADED Framework — สำหรับสัมภาษณ์ System Design
RESHADED เป็น Framework ที่ช่วยให้คุณตอบคำถาม System Design อย่างมีโครงสร้างในการสัมภาษณ์
- R — Requirements: ถามให้ชัดเจนว่า Functional Requirements (ระบบทำอะไรได้บ้าง) และ Non-functional Requirements (Performance, Scale, Availability) คืออะไร
- E — Estimation: ประมาณ QPS, Storage, Bandwidth, Memory ที่ต้องการ
- S — Storage Schema: ออกแบบ Data Model, เลือก Database (SQL vs NoSQL), กำหนด Schema
- H — High-level Design: วาด Architecture Diagram แสดง Component หลักๆ และการเชื่อมต่อ
- A — API Design: กำหนด API Endpoints, Parameters, Response Format
- D — Detailed Design: ลงรายละเอียด Component สำคัญ เช่น Algorithm, Data Flow, Edge Cases
- E — Evaluate: ประเมิน Bottlenecks, Single Points of Failure, Trade-offs
- D — Distinctive Component: เพิ่ม Component พิเศษที่ทำให้ระบบโดดเด่น เช่น Analytics, ML Ranking
Monitoring และ Observability
ระบบที่ออกแบบมาดีแค่ไหน ถ้าไม่มี Monitoring ก็เหมือนขับรถโดยปิดตา ต้องมี 3 Pillars ของ Observability
- Metrics: ตัวเลขที่วัดได้ เช่น QPS, Latency P50/P95/P99, Error Rate, CPU/Memory Usage เก็บใน Time-series Database เช่น Prometheus แล้วแสดงผลด้วย Grafana
- Logs: บันทึกเหตุการณ์ที่เกิดขึ้น เก็บแบบ Structured Logging (JSON) แล้วรวมศูนย์ด้วย ELK Stack หรือ Loki
- Traces: ติดตาม Request ตั้งแต่ต้นจนจบข้าม Service ใช้ Distributed Tracing เช่น Jaeger, Zipkin, OpenTelemetry
การตั้ง Alert ที่ดีต้องแจ้งเตือนเฉพาะเรื่องที่ต้องการ Action ไม่ใช่ Alert ทุกเรื่อง Alert Fatigue เป็นปัญหาจริงที่ทำให้ทีมเพิกเฉย Alert สำคัญ
สรุป
System Design เป็นทักษะที่ต้องใช้เวลาฝึกฝนและสะสมประสบการณ์ ไม่มีทางลัด สิ่งสำคัญที่สุดคือการเข้าใจ Trade-offs ของทุกการตัดสินใจ ไม่มี Architecture ที่ดีที่สุด มีแต่ Architecture ที่เหมาะกับบริบทมากที่สุด
สำหรับคนที่เตรียมสัมภาษณ์ ให้ฝึกออกแบบระบบบ่อยๆ ลองออกแบบระบบที่คุณใช้ทุกวัน เช่น Instagram, YouTube, Uber, LINE จะช่วยให้เข้าใจ Pattern ต่างๆ ได้ดี และสำหรับคนที่ทำงานจริง อย่าลืมว่าการ Over-engineer เป็นปัญหาที่พบบ่อยพอๆ กับ Under-engineer เริ่มจากสิ่งที่ง่ายที่สุดที่ตอบโจทย์ แล้วค่อยเพิ่มความซับซ้อนเมื่อเจอปัญหาจริง
หลักการสำคัญที่ควรจำ: KISS (Keep It Simple, Stupid) สำหรับเริ่มต้น แล้วค่อยปรับปรุงเมื่อมีข้อมูลและ Metric บอกว่าจุดไหนเป็น Bottleneck
