CREATE TABLE idempotency_keys (
tenant_id uuid NOT NULL,
key text NOT NULL,
request_hash text NOT NULL,
status text NOT NULL, -- processing | done
response jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, key)
);
System Design
draftedPhương pháp phỏng vấn system design + bài toán kinh điển: rate limiter, chat, checkout, catalog ingestion, multi-tenant SaaS.
0/62 rated · mastery 0%
Bài học
12 bài · ~166 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~15 phút· 4 câu
Khung phỏng vấn system design: requirements, scope và judgment
Một buổi system design 45 phút được chấm theo quy trình chứ không theo đáp án: chia thời gian 8 bước, biến từng non-functional requirement thành một quyết định kiến trúc (kèm bảng downtime tính thật), cắt scope một đề mơ hồ như 'design Instagram', và trả lời câu build vs buy bằng tiêu chí thay vì cảm tính.
- 02~11 phút· 4 câuverify
Back-of-envelope estimation: QPS, storage, connections, latency
Ước lượng bậc độ lớn để chọn kiến trúc: bảng latency và số đo thật trên máy (Redis GET, PK lookup, N+1 so với batch), công thức QPS/storage/keyspace tính từng bước cho bài 10M DAU, URL shortener 100M link/tháng và chat 50M DAU, và cách biết con số nào thực sự đổi thiết kế.
- 03~13 phút· 5 câuverify
Building blocks: stateless service, load balancer, cache layers, chọn database, tìm bottleneck
Những viên gạch của mọi thiết kế: điều kiện để scale ngang (và vì sao 12 pod làm Postgres từ chối kết nối, đo thật), L4 vs L7 load balancer và WebSocket, các lớp cache từ browser tới buffer pool, chọn SQL hay NoSQL theo access pattern, và quy trình tìm bottleneck khi DB CPU 95% (phân biệt chờ pool với query chậm bằng số đo).
- 04~16 phút· 6 câuverify
Dữ liệu ở scale: CAP/PACELC, replication lag, sharding, consistent hashing, ID phân tán, multi-region
CAP và PACELC dùng thế nào trong quyết định thật; replication lag tái hiện trên một replica Postgres thật và cách đọc read-your-writes theo LSN (kèm một gotcha về LSN trước commit); range/hash/directory sharding và hot shard; consistent hashing mô phỏng với virtual nodes; auto-increment, UUIDv4, UUIDv7 và Snowflake đo trên Postgres 18; active-passive và active-active multi-region.
- 05~14 phút· 6 câuverify
Async, idempotency và resilience: queue, Idempotency-Key, retry/jitter, circuit breaker, effectively-once, SLO
Khi nào đặt queue giữa hai service; làm POST /payments idempotent bằng claim-first (tái hiện double charge của check-then-act trên Postgres thật); timeout, retry, jitter và vì sao 3 tầng retry thành 64 lần gọi DB; circuit breaker và bulkhead; vì sao 'Kafka exactly-once' không phủ side effect ra ngoài và cách có effectively-once; đưa SLO, error budget và degradation vào thiết kế.
- 06~13 phút· 4 câuverify
Bài toán kinh điển: distributed rate limiter và URL shortener
Năm thuật toán rate limit chạy trên cùng một trace ranh giới cửa sổ; race condition GET/SET cho qua 500/500 request trong khi INCR và token bucket Lua chặn đúng 100 (Redis 8.10 thật); thiết kế rate limiter multi-tenant (rule, Redis Cluster, fail-open/closed, header); URL shortener từ key generation tới redirect path, với một prototype Node + Postgres + Redis chạy bằng curl.
- 07~15 phút· 8 câuverify
Distributed cache, hot key, leaderboard và news feed
Thiết kế một distributed cache (partitioning, replication, eviction, chuyện gì xảy ra khi node chết), cache stampede và cách gom request xuyên pod, phát hiện và xử lý hot key (đo bằng OBJECT FREQ/--hotkeys), leaderboard 10 triệu người chơi trên một sorted set (đo được 754 MB), fan-out on write/read/hybrid với mô phỏng chi phí theo ngưỡng celebrity, và cursor pagination ổn định (OFFSET 1 triệu: 289 ms, keyset: 0,06 ms).
- 08~13 phút· 6 câuverify
Realtime và chat: WebSocket/SSE, Socket.IO nhiều node, ordering, offline sync, streaming giá
Chọn WebSocket, SSE hay long polling; vì sao Socket.IO hỏng khi lên 3 instance (tái hiện 'Session ID unknown' và tin nhắn không tới node khác, rồi sửa bằng Redis adapter); ước lượng chat 50M DAU; thiết kế chat với sequence per conversation, dedupe client_msg_id và resync theo lastSeq (chạy thật trên Postgres); streaming giá có conflation, snapshot + delta và re-auth trên kết nối dài.
- 09~13 phút· 4 câuverify
Checkout, tồn kho và thanh toán: chống oversell, reservation, PSP timeout, saga
Tái hiện đúng con số của đề: 500 sản phẩm bán 540 lần với read-check-write trên Postgres, rồi sửa bằng conditional UPDATE và reservation có TTL (kèm job trả hàng hết hạn); payment state machine chạy thật với webhook trùng và handler 'timeout = failed' đến muộn; PSP timeout là trạng thái không xác định; thiết kế checkout multi-tenant đầy đủ với saga, outbox, reconciliation; và khung trả lời câu CV về module Checkout.
- 10~13 phút· 5 câuverify
File upload, search indexing và catalog ingestion
Upload thẳng lên object storage bằng presigned URL (và một gotcha thật: presigned PUT của AWS SDK v3 không ký Content-Type nếu không yêu cầu), pipeline scan virus/thumbnail/tenant isolation cho file 2 GB, vì sao LIKE '%x%' không phải search, giữ Elasticsearch đồng bộ với DB bằng outbox/CDC + external version + alias swap, thiết kế pipeline ingestion hàng triệu SKU mỗi ngày, và khung trả lời câu CV về Data Enrichment + ES Indexer.
- 11~16 phút· 6 câuverify
Scheduler, time zone, notification và webhook delivery
Distributed job scheduler trên Postgres với FOR UPDATE SKIP LOCKED, lease và reaper (3 dispatcher, 1.000 job, 0 lần chạy trùng, phục hồi job của dispatcher chết); vì sao node-cron gửi báo cáo 3 lần; lưu lịch lặp bằng wall time + IANA zone (tái hiện lỗi ca Sydney lệch giờ khi DST bằng Luxon); shift scheduling với exclusion constraint chống chồng ca; notification đa kênh không trùng; webhook delivery có HMAC, chống replay và SSRF.
- 12~14 phút· 5 câuverify
Multi-tenant SaaS và kiến trúc: tenant isolation, RBAC, ranh giới service, monolith vs microservices
Thiết kế B2B SaaS đa tenant: pool/bridge/silo, tenant resolution có kiểm tra membership, AsyncLocalStorage + Postgres RLS chạy thật (quên filter vẫn an toàn, connection pool không có context thì fail closed, superuser bỏ qua RLS), RBAC cho user thuộc nhiều tenant; rồi các câu judgment: chia ranh giới service và data ownership, modular monolith + queue hay Kafka microservices cho team 15 người, và migration Strangler Fig với event schema tương thích ngược.
Tóm tắt & cheat sheet của track
TL;DR
- System design interview không chấm "đáp án đúng" — chấm quy trình: làm rõ yêu cầu, ước lượng, chọn thành phần có lý do, đào sâu chỗ khó, và nói rõ trade-off.
- Khung 8 bước: requirements → estimates → API → data model → high-level design → deep dive → bottlenecks → trade-offs. Dành 5–8 phút cho requirements; đừng vẽ Kafka ở phút thứ hai.
- Mọi thiết kế đều xoay quanh vài câu hỏi lặp lại: dữ liệu nào là source of truth, consistency cần mạnh tới đâu, chỗ nào hot (key, partition, tenant), chuyện gì xảy ra khi retry/duplicate, và hệ thống degrade thế nào khi một phần chết.
- Với senior: nối thiết kế với kinh nghiệm thật (multi-tenant, checkout, indexer, Socket.IO) — số liệu thật của bạn đáng giá hơn số trong sách.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Functional vs non-functional req | Hệ thống làm gì vs tốt tới mức nào (latency, availability, consistency, scale) | "p99 < 200 ms, 99.9% availability, không oversell" |
| Back-of-envelope | Ước lượng bậc độ lớn để chọn kiến trúc | 10M DAU × 20 req ÷ 86 400 s ≈ 2.3k QPS avg |
| Stateless service | Không giữ state theo user trong process → scale ngang tự do | Session trong Redis, không trong memory |
| Replication | Bản sao dữ liệu để đọc nhiều / chịu lỗi | Primary + 2 read replica, lag vài trăm ms |
| Sharding / partitioning | Chia dữ liệu theo key sang nhiều node | hash(user_id) % 64 |
| Idempotency | Gọi lại nhiều lần = một lần | Idempotency-Key trên POST /payments |
| Fan-out | Đẩy một sự kiện tới nhiều người nhận | Feed: fan-out on write vs on read |
| Backpressure | Bên nhận làm chậm bên gửi khi quá tải | Queue depth → giảm consumer fetch, trả 429 |
| Consistent hashing | Thêm/bớt node chỉ di chuyển ~1/N key | Distributed cache ring với virtual nodes |
Cơ chế hoạt động
Quy trình phỏng vấn
Một kiến trúc web "mặc định" để bắt đầu
Bắt đầu từ đây rồi bỏ bớt / thêm theo yêu cầu — đừng thêm component nào mà không nói được nó giải quyết vấn đề gì.
Back-of-envelope numbers (bậc độ lớn)
| Thao tác | Xấp xỉ |
|---|---|
| L1 cache / main memory reference | ~1 ns / ~100 ns |
| SSD random read | ~100 µs |
| Round trip trong cùng datacenter | ~0.5 ms |
| Redis GET qua mạng (cùng AZ) | ~0.2–1 ms |
| Query DB có index | ~1–10 ms |
| Round trip xuyên lục địa | ~100–150 ms |
| 1 ngày | 86 400 s ≈ 10⁵ s |
| 1M request/ngày | ≈ 12 QPS trung bình |
Peak thường ×2–×5 so với trung bình (flash sale có thể ×50). Luôn nói giả định ra.
Trade-offs
| Quyết định | Lựa chọn A | Lựa chọn B | Chọn A khi |
|---|---|---|---|
| Consistency | Strong (single leader, transaction) | Eventual (replica, async) | Tiền, tồn kho, quyền truy cập |
| Feed | Fan-out on write | Fan-out on read | Đa số user ít follower; celebrity → hybrid |
| Realtime | WebSocket | SSE / polling | Cần hai chiều, độ trễ thấp |
| ID | UUIDv7 / Snowflake | DB sequence | Nhiều writer, không muốn round trip DB |
| Pagination | Cursor (keyset) | Offset | Dataset lớn, dữ liệu thay đổi liên tục |
| Tenant isolation | Pool (shared schema + tenant_id) | Silo (DB riêng) | Nhiều tenant nhỏ; silo cho enterprise/compliance |
| Communication | Sync (HTTP/gRPC) | Async (queue/event) | Cần kết quả ngay, chuỗi ngắn |
Pitfalls
- Nhảy vào vẽ ngay mà không hỏi scale, consistency, read/write ratio.
- Ước lượng xong nhưng không dùng con số để ra quyết định.
- "Thêm cache" mà không nói invalidation, TTL, stampede, cache key theo tenant.
- Bỏ qua retry + duplicate (webhook, queue at-least-once, user bấm hai lần).
- Single point of failure ẩn: một Redis, một cron instance, một region.
- Dùng distributed transaction (2PC) cho mọi thứ thay vì saga + idempotency.
- Lưu thời gian lịch làm việc bằng offset cố định (
+07:00) thay vì IANA time zone. - Không nói về observability và cách hệ thống degrade.
Cheat sheet
- Khung: Req → Estimate → API → Data → HLD → Deep dive → Bottleneck → Trade-off.
- QPS = DAU × req/user/day ÷ 86 400; peak ×2–5. Storage = records × size × retention × replication.
- Rate limiter: token bucket trong Redis bằng Lua (atomic), key theo user/IP/tenant, fail-open vs fail-closed phải quyết định.
- Chat: WebSocket gateway stateless + pub/sub (Redis adapter/Kafka), message store phân vùng theo
conversation_id, sequence per conversation, ack + resend khi reconnect. - Checkout: reserve stock bằng conditional update, idempotency key, payment state machine, webhook + reconciliation, outbox cho event.
- Search indexing: DB là source of truth → CDC/outbox → indexer idempotent (version) → alias swap khi reindex.
- Scheduler: lưu job trong DB/queue, lease + heartbeat, at-least-once + idempotent handler.
- Time zone: lưu IANA zone + local wall time cho lịch lặp; lưu UTC instant cho occurrence cụ thể.
References
- System Design Primer (donnemartin) ↗
- Martin Kleppmann — Designing Data-Intensive Applications ↗
- Latency numbers every programmer should know (interactive) ↗
- Stripe — Designing robust and predictable APIs with idempotency ↗
- Socket.IO — Using multiple nodes ↗
- Redis — Sorted sets ↗
- Elasticsearch — Aliases ↗
- AWS Builders' Library — Timeouts, retries and backoff with jitter ↗
- RFC 9562 — Universally Unique IDentifiers (UUIDv7) ↗
- Martin Fowler — Strangler Fig Application ↗
- Microservices.io — Transactional outbox ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 62 / 62
async function allow(userId: string): Promise<boolean> {
const key = `rl:${userId}:${Math.floor(Date.now() / 60_000)}`;
const count = Number(await redis.get(key) ?? 0);
if (count >= 100) return false;
await redis.set(key, count + 1, 'EX', 60);
return true;
}
app.post('/payments', async (req, res) => {
const key = req.header('Idempotency-Key')!;
const existing = await db.payment.findUnique({ where: { idempotencyKey: key } });
if (existing) return res.json(existing);
const charge = await psp.charge(req.body.amount, req.body.token);
const payment = await db.payment.create({ data: { idempotencyKey: key, chargeId: charge.id } });
res.json(payment);
});
// stored when the shift template was created in May (AEST, UTC+10)
const template = { weekday: 1, startUtc: '23:00', endUtc: '07:00' }; // Sunday 23:00 UTC
function occurrence(date: Date) {
return { start: atUtcTime(date, template.startUtc), end: atUtcTime(nextDay(date), template.endUtc) };
}
const inflight = new Map<string, Promise<unknown>>();
async function getOnce<T>(key: string, load: () => Promise<T>): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
if (!inflight.has(key)) inflight.set(key, load().finally(() => inflight.delete(key)));
return inflight.get(key) as Promise<T>; // coalesce within this process only
}
const io = new Server(httpServer, { cors: { origin: APP_ORIGIN } });
io.on('connection', (socket) => {
socket.on('join', (roomId) => socket.join(roomId));
socket.on('message', (msg) => io.to(msg.roomId).emit('message', msg));
});
// deployed as 3 replicas behind a round-robin load balancer