Chương 83. Checklist thiết kế
Hai chương trước nói về:
Bắt đầu từ yêu cầu thật.
Ước lượng nhanh trước khi thiết kế.
Chọn kiến trúc theo giai đoạn.
Chương này gom lại thành một checklist thiết kế.
Checklist này không phải để biến kiến trúc thành công thức cứng.
Nó là một cách tự hỏi:
Mình đã nghĩ đủ các góc quan trọng chưa?
Khi thiết kế một hệ thống, ta rất dễ tập trung vào một phần mình quen:
API.
Database.
Queue.
Frontend.
AI.
Cloud.
Rồi quên những phần khác:
Transaction boundary.
Failure handling.
Security.
Observability.
Deploy.
Cost.
Thông điệp chính của chương:
> Checklist thiết kế tốt không thay thế tư duy. Nó giúp ta không bỏ sót những câu hỏi căn bản trước khi hệ thống đi vào production: request đi qua đâu, dữ liệu nằm ở đâu, transaction kết thúc ở đâu, job retry thế nào, cache có sai không, lỗi xử lý ra sao, ai được quyền làm gì, quan sát bằng gì, deploy thế nào và chi phí phình ở đâu.
---
83.1. Vì sao cần checklist?
Engineer giỏi vẫn quên.
Không phải vì kém.
Mà vì hệ thống có quá nhiều góc.
Ví dụ khi thiết kế AI Judge, ta có thể nhớ:
Gọi Gemini.
Lưu kết quả.
Hiển thị cho giáo viên.
Nhưng lại quên:
AI timeout thì sao?
Job retry có trừ credit hai lần không?
Prompt version có lưu không?
File upload đã scan chưa?
Queue backlog có alert không?
Tenant A có thấy dữ liệu tenant B không?
Deploy prompt mới rollback thế nào?
Cost/token theo tenant có đo không?
Checklist là bộ câu hỏi giúp ta kéo các rủi ro này ra ánh sáng.
Checklist không cần làm hệ thống nặng hơn.
Nó giúp ta chọn chỗ cần nghiêm túc và chỗ có thể đơn giản.
---
83.2. Cách dùng checklist
Đừng dùng checklist như một bài kiểm tra máy móc.
Không phải mục nào cũng cần giải pháp phức tạp.
Ví dụ:
Cache?
Câu trả lời có thể là:
Chưa cần cache, vì traffic nhỏ và query ổn.
Đó là câu trả lời hợp lệ.
Hoặc:
Queue?
Câu trả lời có thể là:
Chấm AI cần queue, nhưng gửi email có thể dùng cùng job system ban đầu.
Điều quan trọng là có suy nghĩ.
Checklist không bắt ta dùng mọi công nghệ.
Checklist bắt ta không bỏ qua câu hỏi.
---
83.3. Request path
Request path là đường đi của một request từ user đến hệ thống và quay lại.
Ví dụ nộp bài:
Browser
-> CDN/load balancer
-> API
-> Auth middleware
-> Permission check
-> Validation
-> Database
-> Object storage
-> Queue
-> Response
Câu hỏi cần hỏi:
Request bắt đầu từ đâu?
Đi qua những lớp nào?
Lớp nào authenticate?
Lớp nào authorize?
Lớp nào validate?
Lớp nào ghi source of truth?
Lớp nào gọi dependency ngoài?
Lớp nào có thể timeout?
Lớp nào có thể retry?
Response trả cho user khi nào?
Nếu không vẽ request path, ta dễ nhầm:
Trả success trước khi lưu dữ liệu.
Gọi AI lâu trong request.
Không có timeout ở dependency.
Validate ở frontend nhưng backend quên.
Request path là xương sống của thiết kế.
---
83.4. Request path nên có điểm dừng rõ
Một request không nên kéo theo cả thế giới nếu không cần.
Ví dụ submit bài không nên đồng bộ làm hết:
Lưu bài.
Chấm AI.
Gửi email.
Cập nhật dashboard.
Sync LMS.
Tính analytics.
Request nên dừng ở điểm đủ để trả lời user:
Bài đã được nhận và xếp hàng chấm.
Sau đó việc lâu chạy nền.
Điểm dừng này rất quan trọng.
Nó quyết định:
User-facing latency.
Transaction boundary.
Idempotency.
Status model.
Queue design.
Một câu hỏi hay:
Trước khi trả response, điều gì phải chắc chắn đã xảy ra?
Với submit bài:
File/source of truth đã lưu.
Submission id đã tạo.
Timestamp đã ghi.
Job chấm có đường chắc chắn để được tạo.
Những thứ khác có thể xử lý sau.
---
83.5. Data model
Data model là cách hệ thống biểu diễn dữ liệu và quan hệ.
Câu hỏi:
Entity chính là gì?
Quan hệ giữa chúng là gì?
Source of truth là bảng/object nào?
Dữ liệu nào cần version?
Dữ liệu nào cần audit?
Dữ liệu nào cần tenant_id?
Dữ liệu nào có thể soft delete?
Dữ liệu nào cần immutable history?
Với AI Judge:
Tenant.
User.
Membership.
Class.
Assignment.
Rubric.
Submission.
SubmissionAttempt.
GradingJob.
GradingResult.
TeacherReview.
CreditLedgerEntry.
AuditLog.
Nếu thiếu entity quan trọng, logic sẽ bị nhét vào field mơ hồ.
Ví dụ:
submission.score
có thể không đủ nếu cần phân biệt:
AI score.
Teacher final score.
Score version.
Review state.
Rubric version.
Data model tốt làm nghiệp vụ rõ hơn.
Data model kém làm code phải đoán.
---
83.6. Data model nên nói được trạng thái
Nhiều hệ thống thiếu trạng thái rõ.
Ví dụ chỉ có:
score = null
Nhưng score = null có thể nghĩa là:
Chưa chấm.
Đang chấm.
Chấm lỗi.
Cần review.
Không đủ dữ liệu.
Học sinh chưa nộp.
Một state model rõ tốt hơn:
received.
queued.
grading.
graded.
failed.
needs_review.
published.
State rõ giúp:
UI hiển thị đúng.
Worker biết làm gì.
Alert biết khi nào stuck.
Support debug được.
Testing dễ hơn.
Nếu trạng thái chỉ nằm trong suy luận của code, production sẽ rất khó hiểu.
Hãy để dữ liệu nói rõ nó đang ở đâu trong workflow.
---
83.7. Transaction boundary
Transaction boundary trả lời:
Những thay đổi nào phải thành công cùng nhau?
Ví dụ submit bài:
Tạo submission record.
Ghi audit.
Tạo outbox/job record.
Có thể cần trong cùng transaction.
Nhưng gọi AI không nên nằm trong transaction database.
Vì AI call lâu và có thể timeout.
Câu hỏi:
Transaction bắt đầu ở đâu?
Kết thúc ở đâu?
Có gọi network bên ngoài trong transaction không?
Nếu bước sau fail, bước trước xử lý thế nào?
Một lỗi phổ biến:
Ghi database xong rồi publish event, nhưng publish fail.
Kết quả:
Dữ liệu đã đổi nhưng consumer không biết.
Pattern như transactional outbox giúp xử lý.
Transaction boundary là nơi correctness được quyết định.
---
83.8. Transaction không nên quá dài
Transaction dài giữ lock lâu.
Ví dụ nguy hiểm:
BEGIN
ghi submission
upload file lớn
gọi AI 90 giây
ghi result
COMMIT
Trong lúc đó database có thể giữ lock hoặc connection.
Tốt hơn:
Transaction ngắn để lưu source of truth.
Việc lâu đưa ra ngoài.
Ví dụ:
BEGIN
create submission
create outbox grading_requested
COMMIT
Sau đó worker xử lý AI.
Câu hỏi checklist:
Có transaction nào giữ quá lâu không?
Có network call trong transaction không?
Có batch update lớn gây lock không?
Transaction bảo vệ dữ liệu.
Nhưng transaction dùng sai có thể làm database đau.
---
83.9. Queue và job
Queue/job dùng cho việc lâu, retry được, không cần user chờ.
Câu hỏi:
Job nào cần queue?
Job mất bao lâu?
Job có idempotent không?
Retry policy là gì?
Lỗi nào retry?
Lỗi nào fail luôn?
Có DLQ không?
Job có tenant_id không?
Có priority không?
Oldest job age có đo không?
Với AI Judge:
AI grading.
File scan.
LMS sync.
Notification.
Report export.
Analytics update.
Không nên để mọi job chung một queue nếu job nặng có thể chặn job quan trọng.
Ví dụ:
Report export lớn không nên làm grading job chậm trong kỳ thi.
Queue là công cụ tách tải.
Nhưng queue không tự làm hệ thống đáng tin.
Job phải có trạng thái, idempotency và observability.
---
83.10. Job idempotency
Job sẽ retry.
Worker sẽ crash.
Message có thể được xử lý hai lần.
Vì vậy hỏi:
Nếu job chạy hai lần, có hỏng không?
Ví dụ:
Chấm cùng submission hai lần có tạo hai điểm chính thức không?
Gửi email hai lần không?
Trừ credit hai lần không?
Sync LMS hai lần có tạo duplicate không?
Idempotency key nên gắn với hành động nghiệp vụ:
grading_result:submission_id:attempt
charge:submission_id:attempt
notification:grading_completed:submission_id
Nếu job không idempotent, retry là nguy hiểm.
Nếu không retry, lỗi tạm thời làm mất việc.
Thiết kế job tốt là thiết kế để retry an toàn.
---
83.11. Cache
Cache có thể làm hệ thống nhanh hơn.
Nhưng cache cũng có thể làm hệ thống sai.
Câu hỏi:
Dữ liệu này có được stale không?
Stale tối đa bao lâu?
Cache key là gì?
Có tenant/permission trong cache key không?
Invalidation khi dữ liệu đổi thế nào?
Cache miss có làm database sập không?
Thundering herd khi cache expire thì sao?
Với AI Judge:
Rubric có thể cache.
Assignment metadata có thể cache ngắn.
Permission có thể cache rất cẩn thận.
Điểm chính thức cần thận trọng.
RAG retrieval cache phải tenant-aware.
Một lỗi nguy hiểm:
Cache key thiếu tenant_id.
Tenant B có thể nhận dữ liệu tenant A.
Cache là tối ưu hiệu năng.
Nhưng trong hệ thống multi-tenant, cache cũng là boundary bảo mật.
---
83.12. Failure handling
Failure handling trả lời:
Khi một phần hỏng, hệ thống phản ứng thế nào?
Câu hỏi:
Timeout là bao nhiêu?
Retry không?
Retry bao nhiêu lần?
Backoff/jitter có không?
Circuit breaker có cần không?
Fallback là gì?
User thấy trạng thái gì?
Có alert không?
Có runbook không?
Ví dụ AI provider lỗi:
Không để request submit fail nếu bài đã lưu.
Job chuyển retry/queued.
Nếu lỗi kéo dài, chuyển needs_review hoặc delayed.
Alert queue age/provider error.
UI nói bài đã nhận và đang chờ chấm.
Failure handling tốt không che giấu lỗi.
Nó giới hạn thiệt hại và nói thật với user.
---
83.13. Security
Security checklist bắt đầu bằng:
Ai được làm gì với dữ liệu nào?
Câu hỏi:
Authentication ở đâu?
Authorization ở đâu?
Backend có enforce không?
Tenant isolation có chắc không?
Input validation có đủ không?
Secrets lưu ở đâu?
File upload có scan không?
URL user gửi có chống SSRF không?
Audit hành động nhạy cảm có không?
Rate limit/abuse prevention có không?
Không nên chỉ dựa vào frontend.
Không nên chỉ dựa vào prompt AI.
Không nên dùng một API key/DB user toàn quyền cho mọi service nếu có thể tránh.
Với AI app, security còn gồm:
Prompt injection.
RAG permission.
Tool calling permission.
Prompt/response logs.
Provider data policy.
Security nên đi vào thiết kế từ đầu.
Không phải kiểm tra cuối cùng.
---
83.14. Observability
Observability trả lời:
Khi production có vấn đề, ta biết chuyện gì đang xảy ra không?
Câu hỏi:
Log có request_id/correlation_id không?
Metric chính là gì?
Trace request/job qua đâu?
Dashboard nào cho luồng chính?
Alert dựa trên triệu chứng user hay chỉ CPU?
Có deploy marker không?
Có đo cost không?
Có đo queue age không?
Có đo provider latency/error không?
Với AI Judge:
submit_success_rate.
time_to_grade.
oldest_grading_job_age.
provider_timeout_rate.
parse_error_rate.
needs_review_rate.
cost_per_submission.
score_distribution.
teacher_override_rate.
Observability nên gắn với câu hỏi vận hành thật:
Bài chấm chậm vì đâu?
Provider lỗi hay worker thiếu?
Prompt mới làm cost tăng không?
Tenant nào bị ảnh hưởng?
Nếu dashboard không giúp trả lời câu hỏi thật, nó chỉ là tranh treo tường.
---
83.15. Deploy
Deploy checklist hỏi:
Thay đổi này ra production thế nào?
Rollback thế nào?
User nào thấy trước?
Metric nào theo dõi?
Migration có an toàn không?
Câu hỏi:
Có feature flag không?
Có canary không?
Có migration expand-contract không?
Code cũ và mới chạy cùng nhau được không?
Rollback data có cần không?
Smoke test sau deploy là gì?
Deploy lúc nào tránh peak?
Với AI Judge, deploy prompt/model mới cần:
Prompt version.
Offline evaluation.
Shadow/canary.
Cost/latency metrics.
Rollback về version cũ.
Deploy không chỉ là đưa code lên server.
Deploy là cách thay đổi hành vi hệ thống mà không làm user đau quá mức nếu có lỗi.
---
83.16. Cost
Cost checklist hỏi:
Tiền phình ở đâu?
Câu hỏi:
Tài nguyên đắt nhất là gì?
Cost theo request/job/tenant là bao nhiêu?
AI token cost có đo không?
Retry có nhân cost không?
Storage/log retention có hợp lý không?
Search/vector index có tăng không?
Tenant free có thể đốt cost không?
Có quota/budget alert không?
Với AI Judge:
AI calls.
Token input/output.
Prompt logs.
File storage.
Embeddings.
Shadow evaluation.
Retry.
Cost không nên chỉ xem cuối tháng.
Cost nên là metric thiết kế.
Nếu một feature càng được dùng càng lỗ, đó không chỉ là vấn đề business.
Đó cũng là vấn đề kiến trúc.
---
83.17. Checklist áp dụng cho một luồng: nộp bài
Áp dụng nhanh cho luồng nộp bài:
Request path:
Browser -> API -> Auth -> Permission -> Validate -> Store file -> DB -> Queue -> Response.
Data model:
Submission, SubmissionAttempt, File, GradingJob, AuditLog.
Transaction:
Create submission + outbox/job record trong transaction ngắn.
Queue:
AI grading async, idempotent theo submission attempt.
Cache:
Rubric cache tenant-aware.
Failure:
File upload fail thì không báo đã nộp.
AI fail thì submission vẫn queued/needs_review.
Security:
Tenant permission, file validation, rate limit submit.
Observability:
submit latency, submit success, queue age, job fail rate.
Deploy:
Migration thêm status backward-compatible.
Cost:
Mỗi submission có thể tạo AI cost, cần quota/ledger nếu tính tiền.
Một luồng đơn giản khi đi qua checklist sẽ hiện ra rất nhiều quyết định quan trọng.
---
83.18. Checklist áp dụng cho một luồng: chấm AI
Request/job path:
Worker -> load submission/rubric -> build context -> model gateway -> parse -> validate -> save result.
Data model:
GradingJob, GradingResult, PromptVersion, ModelVersion, RubricVersion, ReviewState.
Transaction:
Save result + ledger charge + audit cần boundary rõ.
Queue:
Retry timeout/5xx, không retry invalid input vô hạn.
Cache:
Rubric/context cache nhưng phải version-aware.
Failure:
Provider timeout, JSON invalid, safety block, cost limit, needs_review.
Security:
Không đưa dữ liệu thừa vào prompt, RAG permission-aware.
Observability:
provider latency, token, cost, parse error, score distribution.
Deploy:
Prompt/model rollout bằng flag/canary.
Cost:
Token budget, retry cost, per-tenant quota.
Checklist giúp AI pipeline không bị giản lược thành:
Gọi model rồi lưu.
---
83.19. Khi checklist phát hiện rủi ro
Checklist không chỉ để viết tài liệu.
Nó nên tạo hành động.
Ví dụ phát hiện:
Job chấm bài không idempotent.
Hành động:
Thêm idempotency key theo submission_attempt_id.
Phát hiện:
Cache key thiếu tenant.
Hành động:
Sửa cache key và thêm test cross-tenant.
Phát hiện:
Prompt mới không có rollback.
Hành động:
Thêm prompt versioning và feature flag.
Nếu checklist chỉ để tick cho xong, nó vô dụng.
Checklist tốt thay đổi thiết kế trước khi lỗi vào production.
---
83.20. Đừng biến checklist thành quan liêu
Checklist quá nặng sẽ bị bỏ qua.
Nếu mỗi thay đổi nhỏ đều phải viết tài liệu 20 trang, team sẽ ghét checklist.
Nên chia theo mức rủi ro.
Thay đổi nhỏ:
Checklist ngắn.
Thay đổi chạm dữ liệu quan trọng, tiền, AI, permission, migration:
Checklist đầy đủ hơn.
Thay đổi enterprise/compliance:
Review kỹ hơn.
Mục tiêu không phải tạo thủ tục.
Mục tiêu là dùng đúng lượng suy nghĩ cho đúng mức rủi ro.
Một checklist tốt làm team nhẹ đầu hơn.
Không làm team mệt thêm.
---
83.21. Checklist thiết kế tổng hợp
Trước khi chốt thiết kế, hãy hỏi:
Request path:
- Request đi qua những lớp nào?
- Trước khi trả response, điều gì đã chắc chắn xảy ra?
- Có network call lâu trong request không?
Data model:
- Entity chính là gì?
- Source of truth ở đâu?
- Trạng thái workflow có rõ không?
- Dữ liệu nào cần version/audit/tenant_id?
Transaction:
- Thay đổi nào phải atomic?
- Transaction có quá dài không?
- Có cần outbox không?
Queue/job:
- Việc nào chạy nền?
- Job có idempotent không?
- Retry/DLQ/priority/tenant context thế nào?
Cache:
- Dữ liệu có stale được không?
- Cache key có tenant/permission/version không?
- Invalidation thế nào?
Failure:
- Timeout/retry/fallback/circuit breaker ra sao?
- User thấy gì khi dependency lỗi?
- Có alert/runbook không?
Security:
- Auth/permission/backend validation ở đâu?
- File/URL/input có an toàn không?
- Secret và dữ liệu nhạy cảm được bảo vệ thế nào?
Observability:
- Log/metric/trace nào giúp debug luồng này?
- Alert có gắn với trải nghiệm user không?
- Cost/queue/provider có được đo không?
Deploy:
- Migration có an toàn không?
- Có flag/canary/rollback không?
- Code cũ và mới có tương thích không?
Cost:
- Tài nguyên đắt nhất là gì?
- Có quota/budget/retention không?
- Retry hoặc abuse có làm cost phình không?
---
83.22. Kết luận của chương
Checklist thiết kế là cách gom tư duy kiến trúc vào một quy trình dễ dùng.
Nó không bảo đảm hệ thống đúng.
Nhưng nó làm giảm khả năng bỏ sót các câu hỏi quan trọng.
Một thiết kế tốt nên trả lời được:
Request đi qua đâu?
Dữ liệu nằm ở đâu?
Transaction kết thúc ở đâu?
Việc lâu chạy thế nào?
Cache có sai không?
Khi lỗi thì sao?
Ai được quyền làm gì?
Quan sát bằng gì?
Deploy thế nào?
Chi phí phình ở đâu?
Thông điệp cần nhớ:
> Checklist không thay thế kinh nghiệm. Checklist là cách biến kinh nghiệm thành thói quen kiểm tra, để mỗi thiết kế mới không phải học lại bằng sự cố production.
Ở chương tiếp theo, ta sẽ nói về checklist production: timeout, retry, health check, alert, backup, rollback, secrets, rate limit, capacity và runbook trước khi một hệ thống thật sự được xem là sẵn sàng.