Mục đíchCách dùng hai bài giải mẫu này
Đây không phải hai trong số 12 đề chính thức của Problem Bank — cố ý chọn hai bài toán khác, cùng độ khó tương đương, để cohort thấy cách lập luận mà không thuộc lòng đáp án cho bài sẽ bị chấm.
Dùng khi nào
- Phát cho cả cohort trước buổi Dojo đầu tiên, đọc ở nhà hoặc đọc chung 20 phút đầu buổi kick-off
- Người điều phối vẽ lại Ví dụ 2 trực tiếp trên bảng trong buổi kick-off — xem chữ biến thành hình vẽ tay quan trọng hơn đọc chữ
- Dùng lại bất cứ khi nào một người trong cohort bị kẹt không biết bắt đầu từ đâu
Không dùng để làm gì
- Không phải khuôn để chép cấu trúc cho 12 đề chính thức — mỗi bài toán có ràng buộc riêng, áp khuôn có sẵn là dấu hiệu SA0–SA1
- Không phải "đáp án đúng duy nhất" — phần cuối mỗi ví dụ nêu rõ phương án khác vẫn có thể chấp nhận được nếu lập luận chặt
Cách đọc nâng caoĐọc một bài giải như thế nào?
Mỗi ví dụ cần được đọc theo năm lớp: giả định → tính toán → phương án → phản biện → bằng chứng. Không chỉ đọc phần phương án được chọn.
| Lớp đọc | Câu hỏi người học cần tự hỏi | Liên kết rubric |
|---|---|---|
| Giả định | Thông tin nào là dữ kiện, thông tin nào đang được giả định? | Trụ 1 · Trụ 2 |
| Tính toán | Con số nào quyết định bottleneck hoặc loại phương án? | Trụ 2 |
| Phương án | Có ít nhất hai lựa chọn thực tế không? Vì sao chọn một? | Trụ 1 · Trụ 4 · Trụ 5 |
| Phản biện | Thiết kế thay đổi thế nào khi ràng buộc hoặc failure mode thay đổi? | Trụ 3 · Trụ 5 |
| Bằng chứng | Điều gì cần load test, spike, ADR hoặc review stakeholder để xác nhận? | Trụ 5 · Trụ 6 |
VÍ DỤ 1 · Mức SA1 · Trụ 1, 2, 5Cảnh báo hợp đồng B2B sắp hết hạn
Bối cảnh nghiệp vụ
Đội kinh doanh B2B hiện phải tự tra bảng tính để biết hợp đồng nào sắp hết hạn, dẫn tới nhiều trường hợp gia hạn trễ. Yêu cầu: hệ thống tự động rà soát hằng ngày và cảnh báo cho sales phụ trách khi hợp đồng còn 30, 15, hoặc 7 ngày là hết hạn — qua email và thông báo trong ứng dụng CRM.
Ràng buộc định lượng
- ~40.000 hợp đồng đang hoạt động, tăng khoảng 15%/năm
- ~500 nhân viên kinh doanh nhận cảnh báo
- Cảnh báo phải xuất hiện trước 8 giờ sáng cùng ngày để sales xử lý trong ngày làm việc
- Dùng lại Mail Gateway và bảng Notifications đã có sẵn trong hệ thống CRM
Bước 1 — Làm rõ ràng buộc trước khi thiết kế
Đây là bước mà người mới hay bỏ qua. Trước khi vẽ bất cứ gì, SA nên hỏi và ghi lại câu trả lời:
| Câu hỏi đặt ra | Câu trả lời giả định (ghi rõ đây là giả định) |
|---|---|
| Cảnh báo có cần đúng-thời-gian-thực không, hay chạy theo lô hằng ngày là đủ? | Theo lô — nghiệp vụ chỉ cần thấy cảnh báo trong ngày, không cần tức thì |
| Một hợp đồng có thể có nhiều sales phụ trách không? | Không — mỗi hợp đồng có đúng một sales sở hữu |
| Nếu job chạy lỗi giữa chừng, có được phép gửi trùng cảnh báo không? | Không được trùng — sales sẽ mất niềm tin vào hệ thống nếu bị nhắc lặp lại |
| Ràng buộc availability của Mail Gateway hiện tại? | SLA nội bộ 99%, không do team này kiểm soát |
Bước 2 — Napkin math
40.000 hợp đồng, rà soát 1 lần/ngày lúc 6:00, hạn chót xong trước 8:00
→ Ngân sách thời gian: 2 giờ = 7.200 giây
Giả sử mỗi hợp đồng cần 1 phép so sánh ngày + ghi tối đa 1 dòng thông báo:
Ngay cả với 50 ms/hợp đồng (chậm, không tối ưu) → 40.000 × 0,05s = 2.000s
Vẫn nằm trong ngân sách 7.200s, kể cả khi không tối ưu gì cả.
So với trần Postgres write đơn giản (vài nghìn/s):
40.000 dòng ghi trong 2 giờ ≈ 5,6 write/s trung bình
→ Cách trần hàng nghìn lần. Đây KHÔNG phải bài toán scale.
Bước 3 — Hai phương án được cân nhắc
| Phương án | Mô tả | Đánh đổi |
|---|---|---|
| A — Batch job hằng ngày Được chọn |
Cron job 6:00 sáng, quét bảng hợp đồng, tính hợp đồng nào rơi vào mốc 30/15/7 ngày, ghi vào bảng Notifications và gọi Mail Gateway gửi email. | Ưu: đơn giản, dùng hạ tầng có sẵn, dễ debug, dễ vận hành. Nhược: nếu job chạy quá 6:00–8:00 (ví dụ do DB chậm bất thường) thì trễ SLA; cần cơ chế idempotent để job chạy lại không gửi trùng. |
| B — Event-driven liên tục | Mỗi lần hợp đồng được tạo/sửa, emit event; một service riêng liên tục tính ngày còn lại và lên lịch cảnh báo. | Ưu: lý thuyết "hiện đại" hơn, phản ứng ngay khi dữ liệu đổi. Nhược: phức tạp hơn hẳn cho một bài toán không cần thời gian thực — cần vận hành thêm một service, thêm điểm hỏng, thêm chi phí bảo trì, mà không giải quyết bài toán nào tốt hơn phương án A ở scale này. |
Bước 4 — Idempotency, phần dễ bị quên nhất
Ràng buộc "không gửi trùng" ở Bước 1 quyết định một chi tiết thiết kế quan trọng: mỗi lần job chạy phải kiểm tra hợp đồng này đã được cảnh báo ở đúng mốc này chưa trước khi gửi — ví dụ bằng một cột last_alert_milestone trên bảng hợp đồng, không dựa vào "job chỉ chạy một lần" (giả định đó sẽ sai vào đúng ngày cần đúng nhất).
Bước 5 — Mô tả kiến trúc để vẽ tay lại
last_alert_milestone trước khi ghi) → Notification Sender đọc dòng mới, gọi Mail Gateway (có sẵn) gửi email, đồng thời dòng đó hiển thị trong CRM (in-app).Luồng lỗi: nếu Mail Gateway lỗi, dòng Notification vẫn được đánh dấu "đã tạo" (sales thấy trong app) nhưng "gửi email: lỗi, sẽ retry" — tách hai trạng thái để không mất thông báo trong app chỉ vì email lỗi.
Bước 6 — ADR mẫu
# ADR-001 — Dùng batch job hằng ngày cho cảnh báo hết hạn hợp đồng
Trạng thái: Đã chấp nhận
Người quyết: {SA} Người tham vấn: {TL CRM, PM Sales Ops}
## Bối cảnh
Cần cảnh báo sales khi hợp đồng còn 30/15/7 ngày hết hạn, trước 8:00 sáng.
40.000 hợp đồng, không có yêu cầu thời gian thực từ nghiệp vụ.
## Các phương án đã cân nhắc
A. Batch job hằng ngày, quét toàn bộ bảng hợp đồng lúc 6:00
B. Event-driven, tính lại liên tục mỗi khi hợp đồng thay đổi
## Quyết định
Chọn phương án A.
## Vì sao loại phương án B
Ở quy mô 40.000 hợp đồng và ngân sách 2 giờ, batch job chạy dưới 2.000
giây kể cả không tối ưu — cách trần thời gian yêu cầu rất xa. B thêm một
service phải vận hành, thêm điểm hỏng, mà không giải quyết vấn đề nghiệp
vụ nào tốt hơn A ở scale hiện tại. B chỉ đáng cân nhắc nếu nghiệp vụ đổi
yêu cầu sang "cảnh báo tức thì khi hợp đồng gần hết hạn", điều chưa xảy ra.
## Hệ quả
Tích cực: triển khai nhanh trong 1 sprint, dùng hạ tầng có sẵn
Tiêu cực: nếu job chạy trễ do DB chậm bất thường, có rủi ro trễ SLA 8:00
Nợ kỹ thuật chấp nhận: không
## Khả năng đảo ngược
Type 2 — đảo ngược được. Nếu sau này nghiệp vụ cần cảnh báo tức thì,
có thể chuyển sang phương án B mà không ảnh hưởng dữ liệu đã có.
## Cần xem lại khi
Số hợp đồng vượt 500.000 (ngân sách 2 giờ không còn dư dả) HOẶC
nghiệp vụ yêu cầu cảnh báo trong vòng vài phút thay vì trong ngày.
Tự kiểm tra trước khi chuyển sang Ví dụ 2
- Bạn có tự hỏi "bài toán này có thật sự cần kiến trúc phức tạp không" trước khi vẽ chưa?
- Bạn có làm napkin math trước khi chọn phương án chưa, hay chọn theo cảm tính?
- ADR của bạn có phần "vì sao loại phương án khác" cụ thể, hay chỉ nói chung chung "phương án kia phức tạp hơn"?
VÍ DỤ 2 · Mức SA2–SA3 · Trụ 2, 3, 5Giám sát trạng thái thiết bị đầu cuối gần thời gian thực
Bối cảnh nghiệp vụ
Bộ phận vận hành muốn một dashboard hiển thị trạng thái online/offline của modem/router khách hàng theo thời gian gần thực, phân theo khu vực địa lý, để chủ động phát hiện sự cố hạ tầng vùng và điều phối kỹ thuật hiện trường trước khi khách hàng gọi lên tổng đài phàn nàn.
Ràng buộc định lượng
- ~2 triệu thiết bị, mỗi thiết bị gửi heartbeat mỗi 60 giây trong điều kiện bình thường
- Khi một trạm mạng khu vực gặp sự cố, có thể hàng chục nghìn thiết bị mất kết nối gần như đồng thời
- Dashboard phải phản ánh trạng thái theo vùng trong vòng vài giây
- Cần lưu lịch sử thay đổi trạng thái 90 ngày để tính SLA uptime cho từng khách hàng
- Khi thiết bị mất kết nối rồi có lại (ví dụ sau sự cố diện rộng), toàn bộ thiết bị trong vùng đó gửi lại heartbeat gần như cùng lúc
Bước 1 — Làm rõ ràng buộc trước khi thiết kế
| Câu hỏi đặt ra | Câu trả lời giả định |
|---|---|
| "Vài giây" cho dashboard nghĩa là p95 hay tuyệt đối? | p95 dưới 5 giây — chấp nhận một số ít trường hợp chậm hơn khi tải cực đỉnh |
| Lịch sử 90 ngày cần lưu mọi heartbeat hay chỉ lúc đổi trạng thái? | Chỉ lúc đổi trạng thái (online↔offline) — đây là câu hỏi quyết định toàn bộ kiến trúc, xem Bước 2 |
| Hệ thống có phải là nguồn duy nhất để tính SLA hợp đồng không, hay chỉ tham khảo vận hành? | Tham khảo vận hành trong giai đoạn 1; tính SLA hợp đồng chính thức là bài toán riêng, không trong phạm vi này |
| Có ràng buộc phải xử lý đúng 100% mọi heartbeat không, hay được phép mất một phần khi quá tải? | Được phép mất một phần heartbeat khi quá tải cực đỉnh — dashboard là công cụ vận hành, không phải hệ thống tính cước |
Bước 2 — Napkin math
2.000.000 thiết bị, heartbeat mỗi 60 giây
→ TPS trung bình = 2.000.000 / 60 ≈ 33.333 heartbeat/giây
So với trần thành phần (xem Performance & Scale Handbook §02):
Postgres write đơn giản vài nghìn/s → VƯỢT TRẦN ~5–10 lần
Redis (100.000 ops/s) → còn cách trần ~3 lần, CHƯA chạm trần
Kafka 1 broker (trăm nghìn/s) → còn cách trần nhiều lần
Kết luận sớm: ghi trực tiếp mọi heartbeat vào Postgres là bất khả thi ở
scale này ngay từ điều kiện bình thường — chưa cần đợi tới sự cố khu vực.
Kịch bản đỉnh — sự cố khu vực rồi phục hồi (retry storm):
Giả sử 5% thiết bị (100.000) cùng mất kết nối rồi cùng reconnect
trong cửa sổ 10 giây khi trạm mạng phục hồi:
100.000 / 10s = 10.000 reconnect/giây dồn thêm vào đúng thời điểm đó
→ Đỉnh tức thời ≈ 33.333 (nền) + 10.000 (storm) ≈ 43.000/giây
→ Vẫn dưới trần Redis, nhưng gấp hơn 20 lần trần Postgres.
Dung lượng lịch sử (chỉ lưu lúc đổi trạng thái, không lưu mọi heartbeat):
Giả định trung bình mỗi thiết bị đổi trạng thái ~3 lần/ngày
(bao gồm cả nhiễu mạng ngắn) × 2 triệu thiết bị × 90 ngày
≈ 540 triệu dòng sự kiện — nằm trong khả năng một Postgres bình
thường nếu partition theo thời gian, KHÔNG cần time-series DB riêng.
Bước 3 — Hai phương án được cân nhắc
| Phương án | Mô tả | Đánh đổi |
|---|---|---|
| A — Redis cho trạng thái hiện tại + batch diff cho lịch sử Được chọn cho giai đoạn 1 |
Mỗi heartbeat ghi thẳng vào Redis: key = device_id, value = timestamp, TTL = 90 giây (1,5× chu kỳ heartbeat). Dashboard đọc trực tiếp Redis, pre-aggregate theo vùng mỗi vài giây bằng một job nền. Một batch job riêng, chạy mỗi 30 giây, so sánh snapshot Redis hiện tại với snapshot trước để phát hiện thiết bị đổi trạng thái, chỉ những thay đổi này mới được ghi vào bảng lịch sử (Postgres, partition theo tháng). | Ưu: Redis xử lý 43.000/giây dễ dàng (còn cách trần ~2 lần ngay cả ở đỉnh). Không cần vận hành thêm hệ thống stream processing. Luồng lịch sử tách biệt, tải nhẹ, dùng hạ tầng quen thuộc. Nhược: Redis là một điểm phụ thuộc mới — cần cấu hình cluster/replica để không phải SPOF. Batch diff mỗi 30 giây nghĩa là phát hiện đổi trạng thái có độ trễ tối đa 30 giây, không phải tức thời tuyệt đối. TTL phải tính đúng — sai một chút sẽ báo nhầm thiết bị offline dù vẫn online. |
| B — Kafka + stream processing (state store) | Heartbeat đẩy vào Kafka topic, partition theo device_id. Một stream processor (ví dụ Kafka Streams hoặc Flink) giữ trạng thái mới nhất từng thiết bị trong state store nội bộ, phát hiện đổi trạng thái ngay trong luồng xử lý, ghi thẳng "state change event" vào kho lịch sử, đồng thời flush snapshot tổng hợp theo vùng cho dashboard mỗi vài giây. | Ưu: độ trễ phát hiện đổi trạng thái thấp hơn (gần tức thời thay vì tối đa 30 giây). Kiến trúc chịu tải tốt hơn nữa nếu quy mô tăng thêm một bậc. Nhược: vận hành phức tạp hơn hẳn — cần đội ngũ quen thuộc với stream processing, xử lý state store phục hồi sau khi service restart, chi phí hạ tầng và vận hành cao hơn cho một mức cải thiện độ trễ (30 giây → gần tức thời) mà nghiệp vụ chưa yêu cầu. |
Bước 4 — Ba vấn đề không được bỏ sót
| Vấn đề | Cách xử lý trong phương án A |
|---|---|
| Retry storm khi phục hồi | Bắt buộc: client (modem) phải reconnect với backoff có jitter, không đồng loạt gửi lại cùng một giây. Nếu không kiểm soát được phía thiết bị, cần thêm một lớp rate limiter trước Redis dành riêng cho luồng heartbeat (bulkhead) để không ảnh hưởng các luồng khác nếu dùng chung Redis cluster. |
| Redis là single point of failure | Redis Cluster hoặc Sentinel với replica, không chạy Redis đơn lẻ cho một luồng dữ liệu quan trọng vận hành ở quy mô này. |
| Hot region khi sự cố diện rộng | Pre-aggregate theo vùng được tính sẵn bởi batch job nền, không để dashboard tự quét toàn bộ 2 triệu key mỗi lần tải trang — đúng lúc cần xem dashboard nhất (đang có sự cố) cũng là lúc hệ thống tải nặng nhất. |
Bước 5 — Mô tả kiến trúc để vẽ tay lại
Luồng lịch sử: State Diff Job (chạy mỗi 30 giây) → so sánh snapshot Redis hiện tại với snapshot trước → phát hiện thiết bị đổi trạng thái → ghi "state change event" vào Postgres (partition theo tháng, 90 ngày giữ lại) → dùng cho báo cáo SLA vận hành.
Đường nâng cấp đã tính trước (ghi trong ADR, chưa triển khai): nếu độ trễ 30 giây không còn chấp nhận được hoặc quy mô tăng gấp 3–5 lần, thay State Diff Job bằng Kafka + stream processor (phương án B), giữ nguyên luồng heartbeat vào Redis cho dashboard thời gian thực.
Bước 6 — ADR mẫu
# ADR-002 — Dùng Redis + batch diff cho giám sát trạng thái thiết bị
Trạng thái: Đã chấp nhận
Người quyết: {SA} Người tham vấn: {TL Vận hành, Hạ tầng}
## Bối cảnh
2 triệu thiết bị, heartbeat 60s (~33.000/s nền, ~43.000/s đỉnh khi có
retry storm sau sự cố khu vực). Dashboard cần phản ánh trạng thái theo
vùng trong vài giây (p95 < 5s). Lịch sử 90 ngày chỉ cần lưu lúc đổi
trạng thái, không cần lưu mọi heartbeat.
## Các phương án đã cân nhắc
A. Redis cho trạng thái hiện tại + batch job diff mỗi 30s cho lịch sử
B. Kafka + stream processing (state store) cho cả hai luồng
## Quyết định
Chọn phương án A cho giai đoạn 1.
## Vì sao loại phương án B (ở giai đoạn này)
Yêu cầu nghiệp vụ là "vài giây", không phải tức thời tuyệt đối. Redis
đáp ứng đủ tải kể cả ở đỉnh retry storm (43.000/s, còn cách trần Redis
~100.000/s khoảng 2 lần) với độ phức tạp vận hành thấp hơn nhiều so với
việc vận hành thêm một hệ thống stream processing. B là kiến trúc đúng
cho một yêu cầu khắt khe hơn yêu cầu hiện tại — chọn B bây giờ là trả
chi phí vận hành cho độ trễ mà nghiệp vụ chưa cần.
## Hệ quả
Tích cực: triển khai nhanh hơn B đáng kể, dùng năng lực vận hành sẵn có
Tiêu cực: độ trễ phát hiện đổi trạng thái tối đa 30s (chu kỳ batch diff)
Nợ kỹ thuật chấp nhận: có — chấp nhận độ trễ 30s đổi lấy tốc độ triển khai
và chi phí vận hành thấp hơn. Điều kiện trả nợ: xem mục dưới.
## Khả năng đảo ngược
Type 2 — đảo ngược được, có đường nâng cấp rõ ràng sang B mà không phải
thiết kế lại luồng heartbeat vào Redis.
## Cần xem lại khi
Độ trễ 30s không còn chấp nhận được về mặt nghiệp vụ, HOẶC số thiết bị
tăng vượt 6–8 triệu (đỉnh retry storm khi đó có thể tiệm cận trần Redis),
HOẶC xảy ra sự cố Redis cho thấy cấu hình cluster hiện tại không đủ.
Tự kiểm tra trước khi tự làm bài đầu tiên
- Bạn có tách được luồng "cần nhanh" và luồng "cần bền vững" thành hai thiết kế khác nhau chưa, hay đang gộp chung vào một kho dữ liệu?
- Bạn có tính napkin math cho cả điều kiện bình thường lẫn điều kiện đỉnh sau sự cố chưa? Phần lớn lỗi thiết kế nằm ở việc chỉ tính điều kiện bình thường.
- Nếu panel hỏi "vì sao không chọn phương án phức tạp hơn", bạn có câu trả lời gắn với con số cụ thể, hay chỉ nói "đơn giản hơn thì tốt hơn"?
- ADR của bạn có ghi rõ điều kiện để xem lại quyết định không, hay chỉ dừng ở việc chọn phương án?