System Design Interview: Từ Framework Đến Thực Chiến Với Payment System
System Design Interview: Từ Framework Đến Thực Chiến Với Payment System
Bạn đang chuẩn bị cho một buổi phỏng vấn Senior Backend Engineer. Technical rounds đã qua, behavioral cũng ổn, và giờ là vòng quyết định: System Design Interview. Đây là vòng mà nhiều kỹ sư giỏi “ngã ngựa” — không phải vì thiếu kiến thức, mà vì thiếu một framework tiếp cận có hệ thống.
Bài viết này sẽ giúp bạn làm chủ System Design Interview thông qua một case study thực tế: Thiết kế Payment System cho ngân hàng với 10,000 TPS.
Tại sao Payment System là bài toán “khó nhằn”?
Hãy tưởng tượng bạn đang ngồi trong phòng phỏng vấn. Interviewer nói:
“Design X Bank’s payment system. It needs to handle 10,000 transactions per second, support 99.99% uptime, serve 10 million users, and must not allow duplicate charges. You have 45 minutes.”
Vấn đề không nằm ở con số 10,000 TPS. Vấn đề nằm ở sự mơ hồ có chủ đích:
- Yêu cầu không rõ ràng: Domestic hay international? Realtime hay batch?
- Ràng buộc chồng chéo: 99.99% uptime đồng nghĩa với chỉ 52 phút downtime mỗi năm
- Tính đúng đắn tuyệt đối: “Không được charge trùng” — sai một ly, mất cả tỉ đồng
- Áp lực thời gian: 45 phút để cover cả high-level lẫn deep dive
Vậy làm sao để tỏa sáng trong 45 phút đó? Hãy đi theo 5-Step Framework dưới đây.
Framework 5 Bước Chinh Phục System Design Interview
Step 1: Clarify — Đặt câu hỏi trước khi vẽ bất cứ thứ gì
Đây là bước quan trọng nhất mà nhiều ứng viên bỏ qua. Họ lao ngay vào vẽ architecture, và rồi nhận ra mình đã thiết kế cho một bài toán hoàn toàn khác.
Nguyên tắc vàng: Never assume. Always ask.
Hãy chia câu hỏi thành 3 nhóm:
Functional Requirements (Hệ thống làm được gì?)
- Chỉ chuyển tiền nội địa hay có quốc tế?
- Phương thức thanh toán: thẻ, chuyển khoản, ví điện tử?
- Cần xác nhận real-time hay eventual consistency là đủ?
- Có hỗ trợ hoàn tiền không?
Non-Functional Requirements (Scale, latency, consistency?)
- 10,000 TPS là peak hay trung bình?
- Latency yêu cầu: API response < 500ms hay < 2s?
- Multi-region không? Hay chỉ Vietnam?
- Consistency: Strong hay eventual?
Constraints (Giới hạn về công nghệ, team, budget)
- Tech stack preference? Spring Boot, Go, Node.js?
- Team size? 2 người hay 20 người?
- Infrastructure: AWS, on-premise?
- Compliance: PCI-DSS? Thông tư 09 của NHNN?
Sau khi hỏi, hãy tóm tắt lại assumptions của bạn:
“Dựa trên những gì anh/chị chia sẻ, tôi sẽ giả định:
- Chuyển tiền nội địa, cùng ngân hàng (phase 1)
- Xác nhận real-time
- Peak 10,000 TPS, P99 latency < 500ms
- Multi-region: Vietnam trước, có thể mở rộng sau
- PCI-DSS compliance bắt buộc
- Spring Boot, PostgreSQL, Kafka, Redis
Những assumption này có hợp lý không ạ?”
Step 2: Estimate Capacity — Những con số mọi kỹ sư cần thuộc lòng
Trước khi vẽ kiến trúc, bạn cần biết hệ thống của mình đang đối mặt với scale như thế nào. Đây là lúc thể hiện tư duy back-of-the-envelope calculation.
Latency Numbers Mọi Lập Trình Viên Nên Biết
Đây là những con số “kinh điển” mà Jeff Dean từng tổng hợp. Hãy nhớ tương đối:
| Operation | Time |
|---|---|
| L1 cache reference | 0.5 ns |
| Main memory reference | 100 ns |
| Read 4K from SSD | 100 μs |
| Round trip within same datacenter | 500 μs |
| Network round trip California ↔ Netherlands | 150 ms |
| Disk seek | 10 ms |
Bài học: Network chậm hơn memory 150,000 lần. Disk chậm hơn SSD 100 lần. Cache là vua.
Tính toán cho Payment System
Với input: 10,000 TPS peak, 10M users
1. QPS (Queries Per Second)
Daily Active Users: ~1M (10% của 10M)
Requests/ngày: 1M × 10 = 10M requests
Average QPS = 10M / 86,400 ≈ 116 QPS
Peak QPS = 3x average ≈ 350 QPS... nhưng interviewer nói 10,000 TPS peak
→ Dùng luôn 10,000 TPS cho an toàn
2. Database Connections Cần Thiết
Thông thường: 1 connection pool (20 connections) xử lý được ~100 QPS
Với 10,000 TPS: cần 10,000 / 100 = 100 connections
Buffer 30% cho spike: 130 connections
Với pool 20 connections/server: cần ít nhất 7 app servers
Recommend: 10-15 servers cho High Availability
3. Storage Size
Kích thước trung bình 1 transaction: ~52 bytes
Transactions/ngày: 10M
1 năm: 3.6B transactions × 52 bytes ≈ 187 GB
Retention 7 năm (yêu cầu pháp lý): 1.3 TB
4. Cache Size
80/20 rule: 80% requests truy vấn 20% tài khoản active nhất
Cache 20M tài khoản active nhất (balance + info)
Memory/tài khoản: ~128 bytes
Cache size: 20M × 128 bytes ≈ 2.5 GB
Thêm buffer: 5 GB Redis cluster
5. Message Queue (Kafka)
Mỗi transaction sinh 3 events (settlement, notification, audit)
→ 30,000 messages/sec
1 Kafka broker xử lý ~1M msgs/sec → 1 broker đủ, dùng 3 cho HA
Retention 7 ngày: ~127 TB raw → compress còn 10-20 TB
Kết luận sau estimation: Bottleneck sẽ là Database, không phải network hay disk.
Step 3: High-Level Design — Vẽ boxes và arrows
Đừng đi sâu vào chi tiết ngay. Hãy vẽ bức tranh tổng thể trước.
┌─────────────┐
│ User │
│ (Web/App) │
└──────┬──────┘
│ HTTPS
▼
┌─────────────────────────────────────────────────┐
│ API Gateway (AWS ALB / Nginx) │
│ - Rate limiting: 1000 req/sec per user │
│ - SSL termination │
└────────────────┬────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Payment │ │ Payment │ │ Payment │ (10-15 instances)
│Service 1│ │Service 2│ │Service 3│ (Stateless)
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────┼───────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PostgreSQL │◄─────│ PostgreSQL │
│ Primary │ │ Read Replica │
│ (Writes) │ │ (Analytics) │
└──────────────┘ └──────────────┘
┌─────────────────────────────────────────────────┐
│ Kafka Cluster (3 brokers) │
│ Topics: payment_initiated, payment_succeeded, │
│ payment_failed, notification_events │
└─────────────────────────────────────────────────┘
│
├──→ Settlement Service (async)
├──→ Notification Service (email, SMS, push)
└──→ Audit Log Service
┌──────────────────────────┐
│ Redis Cache Cluster │
│ - Account balance cache │
│ - Idempotency cache │
│ TTL: 5 min - 1 hour │
└──────────────────────────┘
API Design — Keep it simple
POST /api/v1/payments/transfer
Request: {
"from_account_id": 123456,
"to_account_id": 654321,
"amount": 1000000,
"currency": "VND",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}
Response: {
"transaction_id": "txn_550e8400...",
"status": "SUCCEEDED",
"amount": 1000000,
"fee": 1000,
"created_at": "2025-03-29T10:30:45Z"
}
Data Model — Entities cốt lõi
-- Account table
CREATE TABLE accounts (
account_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
balance DECIMAL(19,2) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT check_balance CHECK (balance >= 0)
);
-- Transaction table
CREATE TABLE transactions (
transaction_id UUID PRIMARY KEY,
from_account_id BIGINT NOT NULL,
to_account_id BIGINT NOT NULL,
amount DECIMAL(19,2) NOT NULL,
status VARCHAR(20) DEFAULT 'PENDING',
idempotency_key UUID NOT NULL UNIQUE, -- Critical!
created_at TIMESTAMP DEFAULT NOW()
);
-- Idempotency Registry
CREATE TABLE idempotency_registry (
idempotency_key UUID PRIMARY KEY,
transaction_id UUID NOT NULL,
response_payload JSONB NOT NULL,
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '1 hour'
);
Step 4: Deep Dive — Idempotency, bài toán “sống còn”
Đây là phần interviewer sẽ “đào sâu”. Với payment system, idempotency luôn là chủ đề nóng nhất.
Problem: User click “Pay” 3 lần trong 2 giây
Timeline:
T0: User click "Pay 100k"
T0.1: Request 1 đến server, bắt đầu xử lý
T0.5: User không thấy response (mạng chậm), click lại
T0.5: Request 2 đến server
T1.0: Request 1 hoàn thành → charge 100k
T1.1: Request 2 hoàn thành → charge thêm 100k ← DUPLICATE!
T2.0: User hoảng, click lần nữa
T2.5: Request 3 → charge 100k lần thứ 3 ← TRIPLE CHARGE!
Hậu quả: User mất 300k thay vì 100k. Support ticket, chargeback, mất uy tín.
Solution: Idempotency Key Pattern
@Service
public class PaymentTransferService {
public TransferResponse transfer(TransferRequest request) {
String idempotencyKey = request.getIdempotencyKey();
// Step 1: Check cache - đã xử lý key này chưa?
TransferResponse cached = idempotencyRegistry.get(idempotencyKey);
if (cached != null) {
return cached; // Trả về kết quả cũ, không charge lại
}
// Step 2: Xử lý payment thật
TransferResponse response = executeTransfer(request);
// Step 3: Cache kết quả TRƯỚC KHI trả về client
idempotencyRegistry.save(idempotencyKey, response);
return response;
}
@Transactional
private TransferResponse executeTransfer(TransferRequest request) {
// Atomic: tất cả thành công hoặc rollback hết
// 1. Validate & check balance
Account from = accountRepo.findById(request.getFromAccountId());
Account to = accountRepo.findById(request.getToAccountId());
if (from.getBalance() < request.getAmount() + fee) {
throw new InsufficientFundsException();
}
// 2. Update balances
from.debit(request.getAmount() + fee);
to.credit(request.getAmount());
accountRepo.save(from);
accountRepo.save(to);
// 3. Tạo transaction record
Transaction txn = new Transaction(...);
txn.setIdempotencyKey(request.getIdempotencyKey());
transactionRepo.save(txn);
// 4. Outbox Pattern - ghi event vào DB cùng transaction
OutboxEvent event = new PaymentSucceededEvent(txn);
outboxRepo.save(event);
return new TransferResponse(txn);
}
}
Tại sao phải cache TRƯỚC KHI return?
Scenario nguy hiểm:
T0: executeTransfer() thành công
T1: return response cho client (TCP ACK)
T2: Server CRASH trước khi idempotencyRegistry.save()
T3: Client không nhận được response (dù server đã gửi)
T4: Client retry với cùng idempotency key
T5: Cache miss → executeTransfer() chạy lại → DUPLICATE CHARGE!
Solution:
Cache response TRONG CÙNG transaction với executeTransfer()
→ Nếu crash trước khi commit, transaction rollback, không có gì bị charge
→ Nếu commit thành công, cache đã được lưu, retry sẽ hit cache
Outbox Pattern — Đảm bảo event được publish
// Outbox Poller - chạy background mỗi giây
@Scheduled(fixedRate = 1000)
public void pollOutbox() {
List<OutboxEvent> unpublished = outboxRepo.findUnpublished();
for (OutboxEvent event : unpublished) {
try {
kafkaTemplate.send("payment.succeeded", event.getPayload());
outboxRepo.markPublished(event.getId());
} catch (Exception e) {
// Kafka down? Không sao, retry ở lần poll sau
log.error("Failed to publish, will retry");
}
}
}
Tại sao cần Outbox Pattern?
Nếu không có Outbox:
1. Debit account → OK
2. Credit account → OK
3. Send Kafka event → FAIL (Kafka down)
4. Return success cho client
Kết quả: Tiền đã chuyển nhưng notification không bao giờ gửi.
Với Outbox (cùng transaction):
1. Debit account → OK
2. Credit account → OK
3. Save event to outbox table → OK
4. Commit transaction
5. Return success cho client
Outbox Poller sẽ retry gửi Kafka sau.
Step 5: Summarize & Trade-offs — Thể hiện tư duy phản biện
Kết thúc buổi phỏng vấn, interviewer thường hỏi: “What are the bottlenecks in your design?”
Đây là cơ hội để bạn thể hiện critical thinking.
Bottlenecks Identified
1. Database là bottleneck chính
- PostgreSQL single primary: ~50K writes/sec max
- 10,000 TPS đã gần ngưỡng
- Solution: Sharding theo account_id
- Trade-off: Mất khả năng JOIN cross-shard, phức tạp hơn khi query
2. Idempotency cache (Redis) có thể mất dữ liệu
- Redis là in-memory, có thể mất data khi restart
- Solution: Dual-persist vào PostgreSQL (idempotency_registry table)
- Trade-off: Thêm 1 write vào DB, nhưng correctness quan trọng hơn performance
3. Notification có thể bị chậm
- 30,000 messages/sec vào Kafka
- Notification service phải scale để consume kịp
- Solution: Tăng số partitions và consumers
- Trade-off: User không nhận được email ngay lập tức, nhưng payment vẫn thành công
Scaling to 10x (100,000 TPS)
- Database: 10 shards thay vì 3
- Kafka: Thêm partitions (đã support sẵn)
- App servers: Scale horizontally (stateless)
- Cache: Redis cluster scale ngang
Khó nhất: Resharding dữ liệu cũ mà không downtime
Corners Cut (For MVP)
Phase 1:
✓ Domestic only
✓ Real-time confirmation
✓ Không refund
Phase 2:
+ International transfers
+ Refunds
+ Batch payments
Lý do: Start simple, validate thị trường, 80% value từ 20% features.
Những Behavioral Traits Interviewer Đánh Giá
Ngoài technical depth, interviewer còn đánh giá cách bạn suy nghĩ và giao tiếp.
1. Think Out Loud
❌ Bad: Im lặng 5 phút rồi đưa ra solution hoàn chỉnh.
✅ Good:
“OK, 10K TPS là scale đáng kể. Để tôi suy nghĩ qua… Đầu tiên tôi cần clarify requirements: - Domestic only phải không? - Realtime confirmation? - Idempotency có required không? OK, đây sẽ là phần critical. Giờ tôi sẽ estimate capacity…“
2. Draw Before Code
❌ Bad: Nhảy ngay vào viết code Java.
✅ Good: Vẽ diagram trước, giải thích data flow, rồi mới deep dive.
3. State Assumptions Explicitly
❌ Bad: Tự ý giả định mà không nói ra.
✅ Good:
“Tôi đang giả định: 1. Domestic only 2. Real-time required 3. Client tự generate idempotency key 4. Build in-house, không dùng Stripe Những assumption này có hợp lý không?“
4. Unprompted Trade-offs
❌ Bad: Chỉ đưa ra 1 solution duy nhất.
✅ Good:
“Cho database, tôi cân nhắc 3 options: 1. Single PostgreSQL: đơn giản nhưng chỉ scale được ~5K TPS 2. Sharded PostgreSQL: phức tạp hơn nhưng scale được 50K+ TPS 3. NoSQL: scale cực tốt nhưng mất ACID Tôi chọn Sharded PostgreSQL vì payment cần strong consistency.”
5. Ask for Feedback
❌ Bad: Thao thao bất tuyệt 20 phút không dừng.
✅ Good:
“Trước khi đi sâu, anh/chị thấy hướng thiết kế này có hợp lý không? Có component nào anh/chị muốn tôi focus không?”
Tổng Kết: Checklist Làm Chủ System Design
5-Step Framework (45 phút)
| Step | Time | Focus |
|---|---|---|
| Clarify | 5 min | Hỏi functional, non-functional, constraints |
| Estimate | 5 min | QPS, storage, cache, bandwidth |
| High-Level Design | 10 min | Vẽ boxes, arrows, API, data model |
| Deep Dive | 15 min | Chọn 1 component khó nhất (thường là idempotency) |
| Summarize | 5 min | Bottlenecks, scaling, trade-offs |
Công Thức Capacity Estimation Cần Nhớ
QPS = (DAU × requests/user) / 86400
Peak QPS = Average × 3-5
DB Connections = Peak QPS / 100
Cache Size = Hot Data × Object Size
Storage = Records/day × Size × 365 × Retention Years
Pattern Portfolio Tối Thiểu
- ✓ Idempotency Pattern — Mọi operation repeatable
- ✓ Outbox Pattern — Transactional event publishing
- ✓ Circuit Breaker — Fail fast khi downstream down
- ✓ Rate Limiting — Token bucket hoặc sliding window
- ✓ CQRS — Separate read/write models
- ✓ Sharding — Horizontal database scaling
Lời Kết
System Design Interview không tìm kiếm một “perfect answer”. Họ tìm kiếm một kỹ sư biết cách suy nghĩ.
Framework 5 bước này là “la bàn” giúp bạn không bị lạc trong 45 phút áp lực cao. Hãy luyện tập nó với nhiều bài toán khác nhau — từ URL shortener, chat system, đến Uber backend, Netflix — và bạn sẽ thấy pattern lặp lại.
Cuối cùng, hãy nhớ: Interview là conversation, không phải interrogation. Đặt câu hỏi, lắng nghe feedback, và điều chỉnh thiết kế của bạn. Đó chính là điều họ muốn thấy ở một Senior Engineer.
Chúc bạn tự tin và thành công! 🚀