Chương 85. Những lỗi kiến trúc phổ biến

Đến đây, ta đã đi qua gần như toàn bộ nền tảng:

Yêu cầu thật.
Ước lượng nhanh.
Chọn kiến trúc theo giai đoạn.
Checklist thiết kế.
Checklist production.

Chương này nói về những lỗi kiến trúc rất hay gặp.

Không phải lỗi cú pháp.

Không phải thiếu framework.

Mà là những quyết định khiến hệ thống về sau:

Khó hiểu.
Khó vận hành.
Khó scale.
Khó debug.
Khó rollback.
Khó bảo vệ dữ liệu.
Đắt hơn cần thiết.

Điểm thú vị là nhiều lỗi trong chương này đến từ ý định tốt.

Ta muốn hệ thống scale nên dùng microservices quá sớm.

Ta muốn hiện đại nên dùng Kubernetes khi chưa cần.

Ta muốn reliable nên retry thật nhiều.

Ta muốn nhanh nên cache dữ liệu nhưng cache sai.

Ta muốn event-driven nên dùng Kafka cho mọi thứ.

Thông điệp chính của chương:

> Lỗi kiến trúc phổ biến thường không bắt đầu bằng sự lười biếng. Nó bắt đầu bằng việc dùng đúng công cụ sai thời điểm, bỏ qua ranh giới dữ liệu, hoặc quên những nguyên tắc production cơ bản như timeout, idempotency, observability và rollback.

---

85.1. Lỗi 1: Dùng microservices quá sớm

Microservices không xấu.

Nhưng dùng quá sớm rất dễ làm hệ thống phức tạp trước khi có lý do thật.

Dấu hiệu:

Team chỉ có vài người.
Domain còn đổi liên tục.
Chưa có khách hàng thật.
Mỗi service vẫn phải deploy cùng nhau.
Các service dùng chung database.
Không có observability tốt.
Không có contract testing.
Debug một request phải mở log ở 5 nơi.

Lúc này microservices không giải quyết vấn đề.

Nó tạo thêm vấn đề.

Ví dụ AI Judge mới ở MVP nhưng đã tách:

User Service.
Assignment Service.
Submission Service.
Grading Service.
Billing Service.
Notification Service.
Report Service.

Nghe chuyên nghiệp.

Nhưng nếu một thay đổi nhỏ như thêm rubric_version phải sửa 5 service, deploy 5 service, debug 5 log, team sẽ chậm.

Cách tốt hơn thường là:

Modular monolith trước.
Module boundary rõ.
Database boundary suy nghĩ cẩn thận.
Tách service khi ownership/scale/deploy boundary thật sự xuất hiện.

Microservices nên là phản ứng với áp lực thật.

Không phải trang sức kiến trúc.

---

85.2. Lỗi 2: Distributed monolith

Distributed monolith là hệ thống đã tách thành nhiều service nhưng vẫn dính nhau như một monolith.

Dấu hiệu:

Service phải deploy cùng nhau.
Service gọi nhau theo chuỗi dài.
Một request fail nếu một service phụ fail.
Database vẫn dùng chung lung tung.
Không service nào thật sự sở hữu dữ liệu.
Breaking change xảy ra thường xuyên.

Nó lấy nhược điểm của cả hai bên:

Phức tạp như distributed system.
Nhưng coupling như monolith.

Ví dụ:

Submission Service tạo bài.
Grading Service cần gọi Submission Service 5 lần.
Billing Service cần gọi Grading Service để trừ credit.
Notification Service cần gọi cả ba.
Một service chậm kéo cả luồng chậm.

Cách sửa không phải lúc nào cũng là tách thêm.

Đôi khi cần:

Làm rõ ownership dữ liệu.
Giảm synchronous call chain.
Dùng event cho một số side effects.
Đặt contract rõ.
Gộp lại nếu tách sai.

Microservices không phải đích đến.

Ranh giới đúng mới là đích.

---

85.3. Lỗi 3: Dùng Kafka khi Redis/RabbitMQ là đủ

Kafka rất mạnh.

Nó phù hợp với event streaming, log dữ liệu lớn, replay, nhiều consumer, throughput cao.

Nhưng không phải mọi queue đều cần Kafka.

Dấu hiệu dùng Kafka quá sớm:

Chỉ cần chạy job nền đơn giản.
Không cần replay event lịch sử.
Không có đội vận hành Kafka.
Không hiểu partition/consumer group/retention.
Debug message khó.
Mục tiêu chỉ là gửi email hoặc chấm bài async.

Với AI Judge MVP, nếu nhu cầu là:

Đẩy job chấm bài vào queue.
Worker lấy job.
Retry nếu provider timeout.
DLQ nếu fail.

thì Redis queue, RabbitMQ, SQS, Celery/RQ/Sidekiq/Hangfire có thể đủ.

Kafka có thể đáng khi:

Cần event log bền.
Cần replay.
Có nhiều consumer độc lập.
Throughput rất cao.
Analytics/event pipeline nghiêm túc.

Sai không phải là dùng Kafka.

Sai là dùng Kafka khi team chỉ cần một job queue đơn giản và không vận hành nổi Kafka.

---

85.4. Lỗi 4: Dùng Kubernetes khi Docker Compose/PaaS là đủ

Kubernetes rất mạnh.

Nhưng nó là một hệ sinh thái vận hành lớn.

Dấu hiệu dùng quá sớm:

Team nhỏ.
Chưa có traffic lớn.
Không có người vận hành cluster.
Deploy phức tạp hơn code.
Debug production mất nhiều thời gian.
Chi phí hạ tầng tăng.

Nhiều sản phẩm giai đoạn đầu có thể chạy tốt trên:

PaaS.
Managed containers.
Docker Compose trên một VM đủ mạnh.
Managed database.
Managed queue.

Không có gì kém chuyên nghiệp ở việc dùng PaaS nếu nó giúp team tập trung vào sản phẩm.

Kubernetes đáng cân nhắc khi:

Có nhiều service.
Cần autoscaling phức tạp.
Cần deployment strategy tùy chỉnh.
Có đội vận hành/platform.
Cần portability/control cao.

Nếu Kubernetes làm team chậm hơn mà chưa giải quyết nỗi đau thật, nó là over-engineering.

---

85.5. Lỗi 5: Không có timeout

Không có timeout là một trong những lỗi production kinh điển.

Dấu hiệu:

HTTP client dùng default timeout.
Database query có thể chạy mãi.
Worker gọi provider ngoài và chờ rất lâu.
Webhook call không giới hạn.
Job không có max runtime.

Khi dependency chậm, tài nguyên bị giữ.

Ví dụ:

AI provider không trả lời.
100 worker cùng chờ.
Queue không xử lý job mới.
Oldest job age tăng.

Timeout không làm dependency hết lỗi.

Nó giúp hệ thống của ta không bị treo theo.

Cách sửa:

Đặt timeout cho mọi network call.
Đặt statement timeout cho query nặng nếu phù hợp.
Đặt job max runtime.
Chọn timeout theo p95/p99 thực tế và SLO.

Một hệ thống production không có timeout giống như tòa nhà không có cầu dao.

Khi có sự cố, mọi thứ bị kéo theo.

---

85.6. Lỗi 6: Retry không kiểm soát

Retry sai còn nguy hiểm hơn không retry.

Dấu hiệu:

Retry ngay lập tức.
Retry vô hạn.
Mọi lỗi đều retry.
Không có backoff.
Không có jitter.
Không có circuit breaker.
Không có idempotency.

Ví dụ AI Judge:

Provider timeout.
Worker retry ngay.
Nhiều worker retry cùng lúc.
Provider càng quá tải.
Timeout càng tăng.
Queue càng phình.
Cost càng tăng.

Đây là retry storm.

Cách sửa:

Chỉ retry lỗi tạm thời.
Giới hạn số lần.
Exponential backoff.
Jitter.
Circuit breaker khi dependency lỗi hàng loạt.
Idempotency cho side effects.
DLQ cho lỗi không xử lý được.

Retry là thuốc.

Dùng đúng thì cứu.

Dùng quá liều thì làm bệnh nặng hơn.

---

85.7. Lỗi 7: Không có idempotency

Không có idempotency làm retry trở thành nguy hiểm.

Dấu hiệu:

Webhook duplicate tạo giao dịch trùng.
Submit retry tạo hai submission.
Job retry gửi hai email.
Worker crash rồi chạy lại trừ credit hai lần.
LMS sync retry tạo hai điểm.

Production luôn có retry và duplicate.

Vì vậy hành động quan trọng cần idempotency.

Với AI Judge:

submission external_id/client_request_id.
grading_result theo submission_attempt_id.
credit charge theo grading_charge:submission_attempt_id.
webhook event_id.
notification key.

Cách sửa:

Thiết kế idempotency key.
Unique constraint trong database.
Lưu provider_event_id.
Xử lý request trùng bằng cách trả kết quả cũ.
Test duplicate.

Idempotency không phải chi tiết phụ.

Nó là nền của hệ thống có retry, queue và webhook.

---

85.8. Lỗi 8: Cache sai dữ liệu

Cache làm hệ thống nhanh.

Cache sai làm hệ thống nguy hiểm.

Dấu hiệu:

Cache key thiếu tenant_id.
Cache key thiếu permission scope.
Cache không version-aware.
Cache dữ liệu nhạy cảm quá lâu.
Không có invalidation.
Cache stale làm user thấy quyết định cũ.

Ví dụ:

Cache rubric theo assignment_id nhưng assignment_id chỉ unique trong tenant.
Tenant B lấy nhầm rubric tenant A.

Hoặc:

Teacher sửa điểm chính thức.
Cache vẫn trả điểm cũ cho học sinh.

Cách sửa:

Cache key có tenant/user/permission/version nếu cần.
Định nghĩa stale tolerance.
TTL hợp lý.
Invalidation rõ.
Không cache dữ liệu nhạy cảm nếu không cần.
Test cross-tenant.

Cache không chỉ là performance.

Trong hệ thống multi-tenant, cache là security boundary.

---

85.9. Lỗi 9: Không đo p95/p99

Average latency đánh lừa rất giỏi.

Ví dụ:

Average latency = 200 ms.

Nghe ổn.

Nhưng có thể:

p95 = 2 giây.
p99 = 15 giây.

Nghĩa là một nhóm user bị rất chậm.

Với AI Judge:

Average time_to_grade = 3 phút.

Nhưng:

p95 = 20 phút.
p99 = 2 giờ.

thì nhiều học sinh vẫn rất đau.

Cách sửa:

Đo p50, p95, p99.
Đo theo endpoint/job/tenant/provider.
Đo queue age.
Đo tail latency sau deploy.
Alert theo SLO có percentiles.

User không sống trong average.

Những user ở p95/p99 mới thường mở ticket.

---

85.10. Lỗi 10: Không có rollback

Deploy không có rollback là đi một chiều.

Dấu hiệu:

Deploy prompt mới không lưu prompt cũ.
Migration phá schema cũ.
Feature không có flag.
Không biết image version trước đó.
Rollback cần thao tác thủ công phức tạp.
Code cũ không đọc được dữ liệu mới.

Khi production lỗi, team bị kẹt:

Không quay lại được.
Không sửa nhanh được.
User tiếp tục bị ảnh hưởng.

Cách sửa:

Feature flag.
Canary.
Versioned prompt/model.
Deploy artifact version rõ.
Migration expand-contract.
Rollback runbook.
Smoke test sau rollback.

Rollback không phải dấu hiệu thất bại.

Rollback là cơ chế an toàn.

Một team chuyên nghiệp rollback khi cần, rồi điều tra sau.

---

85.11. Lỗi 11: Không có source of truth rõ

Khi nhiều nơi cùng lưu trạng thái, dễ lệch.

Dấu hiệu:

Database, cache, search index, LMS, dashboard mỗi nơi nói một trạng thái.
Không ai biết tin nơi nào.
Không có reconciliation.
Không có owner dữ liệu.

Ví dụ:

Submission Service nói graded.
Dashboard read model nói grading.
LMS nói failed.
Teacher UI nói needs_review.

Cách sửa:

Định nghĩa source of truth cho từng loại dữ liệu.
Read model/cache chỉ là bản phụ.
Có event/reconciliation để cập nhật bản phụ.
Khi lệch, biết tin ai.

Source of truth không rõ làm hệ thống khó debug và khó tin.

Trước khi tối ưu đọc nhanh, hãy biết sự thật gốc nằm ở đâu.

---

85.12. Lỗi 12: Async sai cách

Async không tự làm hệ thống tốt hơn.

Dấu hiệu async sai:

Trả success trước khi lưu dữ liệu quan trọng.
Job không có trạng thái.
Job fail âm thầm.
Queue là nơi duy nhất chứa dữ liệu quan trọng.
Không idempotency.
Không DLQ.
Không alert oldest job age.

Ví dụ nguy hiểm:

API submit trả "đã nhận bài" nhưng submission chỉ nằm trong queue.
Queue mất message.
Bài mất.

Cách sửa:

Lưu source of truth trước.
Queue chỉ vận chuyển công việc.
Job state rõ.
Retry/DLQ/observability.
Idempotency.
User-facing status.

Async tốt là tách việc lâu khỏi request.

Async tệ là giấu lỗi vào nền.

---

85.13. Lỗi 13: Thiếu ownership

Một hệ thống lớn dần mà không có ownership sẽ rối.

Dấu hiệu:

Không ai sở hữu module.
Bug rơi giữa các team.
Service không có owner.
Alert không ai nhận.
Data model không ai dám sửa.
Docs không ai cập nhật.

Kiến trúc không chỉ là code.

Nó phản ánh tổ chức.

Nếu Grading không có owner, prompt/model/cost/queue sẽ rơi vào vùng xám.

Nếu Billing không có owner, ledger và reconciliation sẽ nguy hiểm.

Cách sửa:

Owner cho domain/module/service.
Owner cho alert/runbook.
Code ownership.
Data ownership.
Decision record.

Không có ownership, hệ thống không tiến hóa.

Nó chỉ tích tụ nợ.

---

85.14. Lỗi 14: Bỏ qua migration dữ liệu

Thay code dễ hơn thay dữ liệu.

Dấu hiệu:

Migration chạy một query lớn trên production.
Không dry run.
Không backfill theo batch.
Không đo progress.
Không rollback/compensation.
Code cũ và mới không tương thích.

Ví dụ:

Đổi score thành final_score và xóa column score ngay.
Một số instance cũ vẫn đọc score.
Production lỗi.

Cách sửa:

Expand-contract.
Backfill theo batch.
Verification query.
Deploy code tương thích cũ/mới.
Theo dõi database latency/lock.
Rollback plan.

Migration là production operation.

Không phải chỉ là file trong thư mục migrations.

---

85.15. Lỗi 15: Không tính cost từ đầu

Cost không được đo sẽ phình âm thầm.

Dấu hiệu:

Không biết cost per tenant.
Không biết cost per AI job.
Prompt logs giữ vô hạn.
Metrics cardinality cao.
Retry làm AI cost tăng.
Shadow traffic chạy quá rộng.

Với AI Judge, cost có thể tăng ở:

Model calls.
Token input/output.
Retries.
Prompt logs.
Embeddings.
File storage.
Evaluation/shadow.

Cách sửa:

Đo cost theo feature/tenant/job.
Budget alert.
Quota theo plan.
Token limits.
Retention policy.
Sampling.
Cost review sau deploy.

Cost là một dạng reliability.

Nếu sản phẩm càng dùng càng lỗ, kiến trúc đang có vấn đề.

---

85.16. Lỗi 16: Bỏ qua security vì "chỉ là MVP"

MVP không cần đầy đủ enterprise compliance.

Nhưng không được bỏ qua nền security.

Dấu hiệu nguy hiểm:

Auth sơ sài.
Permission chỉ ở frontend.
Secret hardcode.
Không validate input.
File upload không scan/limit.
Tenant isolation chưa có test.
Support tool xem mọi dữ liệu không audit.

MVP có user thật thì dữ liệu thật đã có trách nhiệm thật.

Cách sửa:

Backend authorization.
Tenant-aware permission.
Input validation.
Secret management.
Basic audit cho hành động nhạy cảm.
Rate limit cơ bản.
Backup dữ liệu quan trọng.

Security cơ bản làm muộn thường đắt hơn làm sớm.

Đừng gọi thiếu nền là tốc độ.

---

85.17. Lỗi 17: Không có runbook

Alert mà không có runbook làm người trực phải tự đoán.

Dấu hiệu:

Alert báo queue backlog nhưng không ai biết kiểm tra gì.
Provider timeout tăng nhưng không biết rollback prompt hay giảm traffic.
Payment mismatch nhưng không biết đối soát từ đâu.
Backup fail nhưng không ai xử lý.

Cách sửa:

Runbook cho alert chính.
Dashboard links.
Các bước kiểm tra.
Ngưỡng rollback.
Owner/escalation.
Sau incident cập nhật runbook.

Runbook không cần dài.

Nó cần hữu ích lúc 3 giờ sáng.

Một hệ thống không có runbook phụ thuộc quá nhiều vào trí nhớ của vài người.

---

85.18. Lỗi 18: Tối ưu trước khi đo

Tối ưu sai chỗ làm hệ thống phức tạp hơn mà không nhanh hơn.

Dấu hiệu:

Thêm cache trước khi biết query nào chậm.
Tách service trước khi biết bottleneck.
Thêm read replica khi vấn đề là N+1 query.
Tăng worker khi bottleneck là provider limit.

Cách sửa:

Đo trước.
Profile query.
Đo p95/p99.
Đo queue age.
Đo provider latency.
Đo cost.

Ví dụ:

Bài chấm chậm.

Đừng vội tăng Celery worker.

Hãy hỏi:

Worker bận hay đang chờ provider?
Provider 429 không?
Prompt mới tăng token không?
Queue enqueue rate bao nhiêu?
Processing rate bao nhiêu?

Đo đúng giúp sửa đúng.

---

85.19. Lỗi 19: Thiết kế không có đường tiến hóa

Over-engineering là sai.

Nhưng tự khóa đường cũng sai.

Dấu hiệu:

Không có tenant_id dù sẽ bán SaaS.
Không lưu prompt version dù AI output cần audit.
Không có status model dù workflow async.
Không có ledger dù có credit.
Không có audit actor dù sẽ cần compliance.

Cách sửa:

Làm đơn giản nhưng giữ ranh giới nền.
Module boundary rõ.
Data model không mù tương lai gần.
Source of truth rõ.
Version/audit cho dữ liệu quan trọng.

Không cần xây enterprise từ ngày đầu.

Nhưng đừng làm những quyết định khiến enterprise gần như không thể sau này.

Thiết kế tốt là đủ đơn giản, nhưng không cụt đường.

---

85.20. Bảng nhìn nhanh

| Lỗi | Dấu hiệu | Cách nghĩ đúng hơn | |---|---|---| | Microservices quá sớm | Team nhỏ, domain chưa rõ | Modular monolith trước | | Kafka quá sớm | Chỉ cần job queue | Dùng queue đơn giản nếu đủ | | Kubernetes quá sớm | Vận hành khó hơn sản phẩm | PaaS/managed trước nếu hợp | | Không timeout | Dependency treo kéo hệ thống | Timeout mọi network call | | Retry vô tội vạ | Retry storm, cost tăng | Backoff, jitter, limit | | Không idempotency | Duplicate side effects | Idempotency key + constraint | | Cache sai | Dữ liệu stale/lộ tenant | Cache key đúng scope | | Không p95/p99 | Average đẹp, user vẫn đau | Đo tail latency | | Không rollback | Deploy lỗi bị kẹt | Flag, canary, expand-contract | | Async sai | Job fail âm thầm | State, retry, DLQ, observability | | Không source of truth | Nhiều sự thật | Định nghĩa sự thật gốc | | Không ownership | Alert/bug không ai nhận | Owner cho domain/service/alert |

---

85.21. Checklist tránh lỗi kiến trúc

Trước khi chọn hoặc thay đổi kiến trúc, hãy hỏi:

  • Vấn đề thật đang giải quyết là gì?
  • Có số liệu chứng minh vấn đề không?
  • Công nghệ này có giải quyết đúng vấn đề không?
  • Team có vận hành được không?
  • Có cách đơn giản hơn đủ dùng không?
  • Dữ liệu quan trọng có source of truth rõ không?
  • Side effect có idempotent không?
  • Dependency ngoài có timeout/retry/circuit breaker không?
  • Có đo p95/p99 không?
  • Có rollback không?
  • Cache có tenant/permission/version-aware không?
  • Queue job có state, retry, DLQ, alert không?
  • Security cơ bản có bị hy sinh không?
  • Cost có được đo không?
  • Ai sở hữu module/service/alert này?
  • Nếu áp lực tăng, kiến trúc có đường tiến hóa không?

Nếu một quyết định kiến trúc không trả lời được những câu này, nên chậm lại một nhịp.

Một nhịp suy nghĩ có thể tiết kiệm nhiều tháng trả nợ.

---

85.22. Kết luận của chương

Những lỗi kiến trúc phổ biến thường rơi vào hai nhóm:

Làm quá phức tạp trước khi có áp lực thật.
Làm quá đơn giản ở những nơi production cần sự chắc chắn.

Dùng microservices, Kafka, Kubernetes quá sớm là nhóm thứ nhất.

Không timeout, retry vô hạn, thiếu idempotency, cache sai, không p95/p99, không rollback là nhóm thứ hai.

Kiến trúc tốt nằm ở giữa:

Đủ đơn giản để team hiểu và vận hành.
Đủ chắc để bảo vệ dữ liệu, tiền, quyền và trải nghiệm user.
Đủ quan sát để biết khi nào đau.
Đủ linh hoạt để tiến hóa khi áp lực thật xuất hiện.

Thông điệp cần nhớ:

> Công nghệ mạnh không cứu được quyết định sai thời điểm. Ngược lại, một kiến trúc đơn giản nhưng có timeout, idempotency, source of truth, observability và rollback thường đáng tin hơn nhiều hệ thống hiện đại nhưng không có nền production.

Ở phần tiếp theo, ta sẽ chuyển sang Atlas Kiến Trúc Hệ Thống Kinh Điển: từng bài toán cụ thể như URL shortener, rate limiter, pastebin, notification system và nhiều hệ thống phổ biến khác.