This commit is contained in:
Canhchimlac
2026-09-15 16:02:30 +07:00
parent d169c85dd4
commit 343ad8bbbc
37 changed files with 9726 additions and 61 deletions

View File

@@ -0,0 +1,206 @@
# ADR-001 — Kiểu kiến trúc tổng thể của sàn e-commerce là Modular Monolith + managed AWS (P2), không phải ~10 Microservices độc lập (P1)
*Tên file: `adr/ADR-001_kieu-kien-truc-tong-the-modular-monolith-p2.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (theo `decision-radar.md` §5 — "Cấu trúc hệ thống" đề xuất SA, chốt SA+Tech Lead; EA phủ quyết nếu lệch chuẩn doanh nghiệp). **Chưa ký thật** — chỉ có Điều phối dự án ký thay theo ngoại lệ `DEC-01`/`DEC-17`, xem §Điều kiện chuyển Accepted |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~7/10** *(chấm lại theo `decision-radar.md §2` — xem §Radar dưới; dưới ngưỡng 8 cứng nhưng vẫn cần POC vì `ASM-07`/`ASM-08` chưa xác minh)* |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-001` · `QAS-005` · `QAS-006` · `QAS-009` · `CON-03` · `CON-05` · `CON-06` · `DRV-04` · `DEC-06` · `POC-01` · `POC-02` · `OQ-010` · `OQ-012` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-001` và đổi trạng thái bản cũ thành `Superseded by`. Không sửa nội dung
> ADR đã Accepted, không xoá ADR đã Rejected.
---
## 1. Bối cảnh
Sàn e-commerce là dự án greenfield (`CTX §4.1`), quy mô mục tiêu "lớn" (`DRV-04`: hàng trăm
nghìn SKU, hàng trăm nghìn–hàng triệu user, đỉnh hàng nghìn–chục nghìn concurrent user mùa flash
sale — kịch bản A 2.000 CCU/~400 đơn/giờ, kịch bản B 10.000 CCU/~15.000 đơn/giờ, `QAS-009`).
`OPT_e-commerce_v1.0.md` (Bước 4 GĐ1) đã dựng và chấm điểm 3 phương án khả thi (P0/P1/P2) cộng
2 phương án bị loại ở gate cứng (P3/P4) — kết luận khuyến nghị **P2** (modular monolith 2–3 nhóm
ECS Fargate + managed AWS, Payment tách riêng), duyệt từng phần bởi PO uỷ quyền tại `DEC-06`,
nhưng `DEC-06` tự ghi rõ **chưa thay thế** yêu cầu `ADR` chính thức (kiểu kiến trúc tổng thể là
quyết định thuộc danh mục bắt buộc `ADR` theo `decision-radar.md §3`). `ASR-001` (chưng cất từ
`DEC-06`) xác nhận đây là ràng buộc cấu trúc bao trùm toàn bộ hệ thống, ép ra ranh giới container
ở `SAD §4`. ADR này **trả nợ** `DEC-06`/`DTM §11 "🟠 Nợ #1"`/`ADL §8` — không phải quyết định
mới, mà là hình thức hoá quyết định đã có hướng đi tạm.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `CON-03` | Cloud AWS bắt buộc (cứng) — loại mọi phương án ngoài AWS ngay từ `OPT` |
| `CON-05` | Số đội/số người thi công **chưa xác nhận thật** — giả định tạm "team MVP chuẩn" (`DEC-02`), tham khảo hồ sơ thầu: BE trung bình ~2,6 FTE/tháng trên 7 tháng, đỉnh 10 người toàn dự án |
| `CON-06` | PCI-DSS SAQ A + NĐ13/2023 — Payment phải là biên cô lập bất kể chọn P1 hay P2 (áp dụng độc lập với ADR này, xem `ADR-002`) |
| `ASR-001` | Ép ranh giới triển khai 2–3 nhóm ECS Fargate + 1 nhóm Payment riêng; ép dùng SQS/EventBridge thay Kafka/MSK; ép 1 RDS chính schema-per-module |
| `QAS-005`/`QAS-006`/`QAS-009` | Thông lượng/độ trễ/khả năng mở rộng phải đạt ở kịch bản A/B — input trực tiếp cho `POC-01`/`POC-02` |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| AWS là cloud bắt buộc | 🟢 Đã kiểm chứng | `CON-03`, xác nhận vòng 3 Q&A brief |
| Payment phải cô lập PCI-DSS SAQ A | 🟢 Đã kiểm chứng | `CON-06`, pháp luật/hợp đồng đối tác |
| SQS FIFO + EventBridge đủ throughput cho `OrderPlaced`/`PaymentConfirmed` ở tải đỉnh | 🔴 Giả định (`ASM-07`) | `POC-01` — **chưa chạy** |
| 1 cụm RDS chính đủ tải ghi/đọc gộp | 🔴 Giả định (`ASM-08`) | `POC-02` — **chưa chạy** |
| Team thi công thật khớp giả định "team MVP chuẩn" (`DEC-02`) | 🔴 Giả định | `OQ-010` — Tech Lead chưa xác nhận |
| P2 rẻ hơn P1 ở TCO 3 năm (sơ bộ, số lượng thành phần) | 🟡 Ước lượng có cơ sở | `TCO §3.2` (chưa quy đổi VND đầy đủ, đơn giá AWS chưa xác nhận `OQ-013`) |
## 2. Phương án đã cân nhắc
*Tham chiếu đầy đủ bảng chấm điểm và sơ đồ mức khối đã có ở `OPT_e-commerce_v1.0.md §3, §4, §5`
— **không lặp lại** ở đây theo đúng ghi chú người duyệt. Tóm tắt lại đúng mức cần để ra quyết
định.*
### PA-1 — P1: Modular microservices (~10 service, database-per-service, Kafka/MSK, OpenSearch)
| | |
|---|---|
| **Mô tả** | ~10 service theo bounded-context, mỗi service 1 RDS logic riêng, Kafka/MSK làm xương sống bất đồng bộ, OpenSearch đa node cho search, ECS/EKS auto-scaling theo domain — theo `SAD.md` v0.2/hồ sơ thầu (`OPT §3` P1) |
| **Ưu** | Đáp ứng `DRV` Must cao nhất (`OPT §4`: điểm 5/5) — scale-out **độc lập** Catalog/Search khỏi Cart/Checkout ngay từ ngày một (`DRV-04`); ranh giới service rõ, dễ thay/rút từng service riêng lẻ về sau |
| **Nhược** | Vi phạm Conway rõ với team giả định hiện tại — mỗi BE ôm trung bình 3–4 service (`CON-05`, `OPT §4` tiêu chí 5: P1=2/5); không có bằng chứng team từng vận hành Kafka/MSK/OpenSearch/EKS production (`CTX §4.4`, `OQ-010` mở); effort dựng nền tảng riêng đã ước 70,2 MD (`WBS-03`, XL, +35% contingency) trước khi làm nghiệp vụ; nhiều thành phần managed độc lập phải vận hành hơn |
| **Chi phí đảo ngược** | Gộp ngược lại thành ít service hơn (nếu team quá nhỏ để vận hành 10 service) tốn công tái cấu trúc network/DB đáng kể — ước > 4 tuần |
### PA-2 — P2: Modular monolith (2–3 nhóm ECS Fargate) + managed AWS, Payment tách riêng *(chọn)*
| | |
|---|---|
| **Mô tả** | Modular monolith theo domain, chạy 2–3 nhóm ECS Fargate (Nhóm Giao dịch / Nhóm Hỗ trợ), SQS FIFO + EventBridge thay Kafka/MSK, 1 RDS chính schema-per-module, Postgres FTS giai đoạn đầu (hoãn OpenSearch); Payment tách hoàn toàn (network/IAM/DB riêng) — theo `OPT §3` P2 |
| **Ưu** | Khớp Conway với team giả định (`OPT §4` tiêu chí 5: P2=5/5); effort nền tảng thấp hơn P1 (-70,2 MD, không cần dựng cụm Kafka/MSK); ít thành phần managed độc lập phải vận hành hơn (chi phí vận hành sơ bộ thấp hơn); rủi ro công nghệ thấp hơn (managed service phổ biến hơn) |
| **Nhược** | Thua P1 đúng 1 bậc ở tiêu chí "Đáp ứng `DRV` Must" (4/5 so với 5/5, `OPT §4`) — scale-out **độc lập** Catalog/Search khỏi Cart/Checkout **kém hơn** ở giai đoạn đầu (chung nhóm "Nhóm Giao dịch"); nếu traffic thật vượt xa dự kiến, tách Catalog/Search ra khỏi monolith muộn sẽ tốn hơn tách từ đầu |
| **Chi phí đảo ngược** | Module hoá rõ theo domain trong code giúp tách thành service riêng về sau **có kế hoạch** khi có bằng chứng tải thật — nhưng tốn công tách hạ tầng (network, DB, CI/CD riêng) tại thời điểm tách, không miễn phí; ước > 4 tuần nếu tách toàn diện |
### PA-0 — Không xây gì (P0, mốc so sánh)
Vi phạm trực tiếp `DRV-01` (Must, blocker MVP) — không phải phương án khả thi, chỉ giữ làm mốc
so sánh (`OPT §4`). Không xét tiếp.
### Bảng so sánh *(tóm tắt — chi tiết đầy đủ 6 tiêu chí × trọng số ở `OPT §4`)*
| Tiêu chí *(trọng số)* | P1 | P2 |
|---|---|---|
| Đáp ứng `DRV` Must (25%) | 5 | 4 |
| Chi phí 3 năm sơ bộ (20%) | 2 | 4 |
| Thời gian tới bản chạy được (15%) | 2 | 4 |
| Rủi ro kỹ thuật (15%) | 2 | 4 |
| Năng lực team & vận hành — Conway (15%) | 2 | 5 |
| Khả năng tiến hoá (10%) | 4 | 3 |
| **Tổng có trọng số** | **2.95** | **4.05** |
Chênh lệch 1.10 điểm — đủ phân biệt theo `OPT §4` (không rơi vào bẫy "chênh <0.3").
## 3. Quyết định
> **Chọn PA-2 (P2) — Modular monolith + managed AWS, Payment tách riêng.**
Toàn sàn e-commerce được tổ chức thành 2–3 nhóm triển khai ECS Fargate (modular monolith theo
domain — Nhóm Giao dịch: Identity & Access, Catalog & Inventory, Cart & Order; Nhóm Hỗ trợ:
Seller Management, Commission & Payout, Promotion & Loyalty, Review, Notification, Shipping &
Fulfillment), cộng 1 Payment Service tách hoàn toàn về mạng/IAM/dữ liệu. Dùng SQS FIFO +
EventBridge (không phải Kafka/MSK) và 1 cụm RDS PostgreSQL chính schema-per-module (không phải
database-per-service).
**Vì sao:** Khớp `CON-05` (Conway — năng lực team giả định hiện tại), giữ nguyên `CON-06` (Payment
cô lập, độc lập với lựa chọn này), và đạt điểm tổng cao nhất trong bộ tiêu chí đã chốt ở `OPT §2`
(4.05 so với 2.95 của P1).
**Phạm vi áp dụng:** Toàn bộ sàn e-commerce, đến khi traffic thật tiệm cận kịch bản B × 2
(~20.000 concurrent) mà chưa kiểm chứng lại bằng POC/bài đo thật (xem `QAS-009` "Ngoài phạm vi").
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| `POC-01` PASS (throughput SQS FIFO/EventBridge ≥100 msg/s sustained, p99 ≤2s, 0 mất message) | Tech Lead + 1 DevOps | **Chưa chạy** (`ARISK-01`, `OQ-012`) |
| `POC-02` PASS (RDS gộp: p95 write ≤300ms, p95 read ≤200ms, CPU <75%) | Tech Lead/DBA | **Chưa chạy** (`ARISK-02`) |
| `OQ-010` — Tech Lead xác nhận team thi công thật khớp (hoặc gần khớp) giả định "team MVP chuẩn" | Tech Lead | Mở |
| AG1 ký thật (PO + Tech Lead, không phải ký thay) | PO + Tech Lead | Mở (`OQ-004`) |
🔴 Dù radar ước lượng ~7 (< 8 ngưỡng cứng), ADR này **vẫn không được chuyển `Accepted`** cho tới
khi cả hai `POC` chạy xong — vì bản thân `ASM-07`/`ASM-08` (nền tảng của toàn bộ lựa chọn P2)
chưa xác minh; đây là cam kết đã ghi tại `OPT §1` "Điều kiện kèm theo" và `DEC-06`.
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| P0 — Không xây gì | Vi phạm trực tiếp `DRV-01` (Must, blocker MVP) | `DRV-01` | Không xét lại — không phải phương án khả thi cho một sàn giao dịch |
| P1 — Modular microservices (~10 service) | Vi phạm Conway với team giả định hiện tại (`CON-05`, mỗi BE ôm 3–4 service); không có bằng chứng team từng vận hành Kafka/MSK/OpenSearch/EKS production (`CTX §4.4`) | `CON-05`, `OPT §4` tiêu chí 4/5 | Nếu `OQ-010` xác nhận team thật có kinh nghiệm Kafka/MSK/OpenSearch/EKS production **và** team đủ lớn (gần đỉnh 10 người đề xuất hồ sơ thầu) — chấm lại theo `OPT §9 OQ-010` |
| P3 — Mua/tích hợp nền tảng mã nguồn mở | Loại ở gate cứng `OPT §2.1` — không kiểm chứng được cô lập Payment cho PCI-DSS SAQ A (`CON-06`) mà không sửa sâu lõi | `CON-06` | Nếu Tech Lead xác nhận kinh nghiệm sâu với một nền tảng cụ thể **và** POC chứng minh cô lập Payment đạt SAQ A — xét lại cho module không nhạy cảm tài chính (`OPT §5`) |
| P4 — Serverless-first (Lambda/Step Functions/DynamoDB) | Cold-start Lambda ảnh hưởng `DRV-02` (bỏ giỏ) chưa có POC đo trong ngữ cảnh VPC+RDS; chưa có bằng chứng năng lực team serverless-at-scale (`CON-05`) | `DRV-02`, `CON-05` | Nếu có POC đo cold-start đạt yêu cầu và Tech Lead xác nhận năng lực — xét lại cho luồng bất đồng bộ thuần (Notification, Commission batch) như lựa chọn lai, không toàn hệ thống (`OPT §5`) |
## 5. Hệ quả
**Hệ quả tích cực**
- Số đơn vị triển khai cần vận hành/on-call giảm từ ~10 (P1) xuống ~3 (2–3 nhóm monolith +
Payment) — khớp trực tiếp FTE DevOps trung bình thấp (~0,7 FTE, `TCO`/hồ sơ thầu)
- Effort tới bản chạy được thấp hơn P1 khoảng 70,2 MD (không cần dựng cụm Kafka/MSK)
- Rủi ro công nghệ thấp hơn — managed service phổ biến hơn (RDS, SQS/EventBridge, ECS Fargate)
**Hệ quả tiêu cực phải sống chung**
- Scale-out **độc lập** Catalog/Search khỏi Cart/Checkout kém hơn P1 ngay từ ngày một — nếu flash
sale gây tải lệch mạnh giữa Catalog và Cart&Order, cả hai vẫn scale chung trong "Nhóm Giao dịch"
cho tới khi tách (xem `ASR-014`/`ADR-012`)
- Nếu `POC-01`/`POC-02` FAIL: phải bổ sung kiến trúc lai (Kafka/MSK riêng cho hot-path đặt hàng,
hoặc tách read replica sớm hơn dự kiến) — không quay lại P1 hoàn toàn (`OPT §1`)
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Tách toàn bộ modular monolith thành microservices sau go-live | Tái cấu trúc network/DB/CI-CD riêng biệt cho từng module — ước > 4 tuần, có rủi ro downtime khi tách |
| Đổi lại SQS/EventBridge sang Kafka/MSK sau khi đã có producer/consumer thật | Viết lại toàn bộ tầng tích hợp bất đồng bộ — xem `ADR-005` (quyết định riêng, không lặp ở đây) |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Chạy `POC-01`/`POC-02` theo tiêu chí PASS/FAIL đã viết trước | Tech Lead + DevOps/DBA | Trước khi ADR này chuyển `Accepted` | `ARISK_e-commerce_v1.0.md §6` |
| Xác nhận `OQ-010` (team thật) | Tech Lead | Trước khi `Accepted` | `00-index/OQ_e-commerce.md` |
| Thêm fitness function kiểm tra ranh giới module (không cho import chéo giữa domain package) | SA + Tech Lead | GĐ3 (`AGD`/`FIT`) | `FIT-01` (ứng viên) |
| Kế hoạch tách Catalog/Search nếu traffic vượt ngưỡng B×2 | Tech Lead | Khi có bằng chứng traffic thật | `TDEBT` (GĐ3) |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Không có import chéo trực tiếp DB giữa các domain package trong monolith (chỉ qua API nội bộ/event) | dependency-cruiser hoặc ArchUnit-style rule | CI | `FIT-01` (ứng viên, GĐ3) |
| Số đơn vị triển khai (ECS service) đúng 2–3 nhóm + 1 Payment, không phình thêm không kiểm soát | Kiểm tra hạ tầng (Terraform plan review) | CI/CD pipeline | `FIT-02` (ứng viên) |
| `POC-01`/`POC-02` — bài đo throughput/tải | k6/artillery, load test staging | Trước go-live, lặp lại trước mỗi major release | — (kết quả ghi vào `ARISK §6`) |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Traffic thật tiệm cận kịch bản B × 2 | ~20.000 concurrent user | Tech Lead/SRE |
| `POC-01` hoặc `POC-02` FAIL | Không đạt tiêu chí PASS đã viết trước (`ARISK §6`) | Tech Lead |
| Team thật khác biệt đáng kể so với giả định "team MVP chuẩn" | Số BE thật < 50% giả định `DEC-02`, hoặc > gấp đôi | Tech Lead/PM |
## 8. Tham chiếu
- POC: `ARISK_e-commerce_v1.0.md §6 POC-01, POC-02`
- Bài đo: chưa chạy — xem §Điều kiện chuyển Accepted
- Tài liệu ngoài: `OPT_e-commerce_v1.0.md` §1, §3, §4, §5 (bảng chấm điểm đầy đủ, không lặp lại) ·
`SAD_e-commerce_v1.0.md §2, §4` (hiện thực hoá quyết định này thành C4 Container)
- Thảo luận: `DEC-06` (00-index/DEC_e-commerce.md), duyệt từng phần 2026-09-12
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có chữ ký thật Tech Lead; `POC-01`+`POC-02` chưa chạy; `OQ-010` (team thật) chưa xác nhận |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,167 @@
# ADR-002 — Payment Service là biên cô lập mạng/IAM/dữ liệu riêng cho PCI-DSS SAQ A
*Tên file: `adr/ADR-002_payment-service-bien-co-lap-pci-dss.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | **Security** (theo `decision-radar.md §5` — "Bảo mật, quyền riêng tư": đề xuất SA, chốt Security, phủ quyết Security). **Chưa có Security thật ký** — chỉ Điều phối dự án ký thay theo `DEC-01`/`DEC-17` |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~7/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-002` · `CON-06` · `BR-CART-05` (BA) · `ADR-001` (độc lập với kiểu kiến trúc tổng thể) |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-002` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Sàn phải chấp nhận thanh toán VNPay/Momo/COD **không lưu thông tin thẻ** để giảm phạm vi tuân
thủ PCI-DSS xuống mức SAQ A (`CON-06`, `DRV-03`) — đây là ràng buộc pháp lý/hợp đồng, không
thương lượng được, và **độc lập với việc chọn P1 hay P2** ở `ADR-001`: dù kiểu kiến trúc tổng thể
là gì, Payment vẫn phải là một biên cô lập. `BR-CART-05` (BA) đã ghi phát biểu nghiệp vụ: hệ
thống sàn không được lưu số thẻ/CVV; mọi xử lý thẻ do VNPay/Momo thực hiện, sàn chỉ lưu
`gateway_transaction_ref` + kết quả.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `CON-06` | PCI-DSS SAQ A (không lưu thẻ) + NĐ13/2023 — cứng, không thương lượng |
| `ASR-002` | Payment phải là ranh giới mạng + IAM + dữ liệu (RDS riêng) hoàn toàn tách biệt |
| `BR-CART-05` (BA) | Không lưu số thẻ/CVV; chỉ lưu `gateway_transaction_ref` + kết quả |
| `QAS-011` | 100% trường PII rà soát trước khi chia sẻ — liên quan gián tiếp (Payment không giữ PII giao hàng) |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| PCI-DSS SAQ A + không lưu thẻ là ràng buộc cứng | 🟢 Đã kiểm chứng | `CON-06`, pháp luật + hợp đồng đối tác |
| Payment cô lập network/IAM/DB là cách duy nhất khả thi để giữ SAQ A trong một kiến trúc chung (monolith/microservices) | 🟡 Ước lượng có cơ sở | Thực hành PCI-DSS phổ biến (segmentation) — chưa có audit chính thức từ QSA/ASV |
| VNPay/Momo có sandbox thật để kiểm chứng luồng redirect+IPN không lưu thẻ | 🔴 Giả định | `CON-04`, chưa có sandbox (`CTX §4.2`) |
## 2. Phương án đã cân nhắc
### PA-1 — Payment là module trong monolith, không tách mạng/IAM riêng
| | |
|---|---|
| **Mô tả** | Payment logic nằm chung namespace/package với Cart & Order trong cùng monolith, chỉ tách bằng ranh giới code (package riêng), dùng chung RDS/network/IAM |
| **Ưu** | Đơn giản triển khai hơn — không cần network segmentation riêng, ít thành phần hạ tầng hơn |
| **Nhược** | **Không kiểm chứng được** phạm vi PCI-DSS thu hẹp — một lỗ hổng ở bất kỳ module nào trong cùng network/IAM có thể mở rộng phạm vi audit PCI-DSS ra toàn bộ hệ thống (vi phạm mục tiêu giảm phạm vi của `DRV-03`) |
| **Chi phí đảo ngược** | Tách ra sau khi đã gộp nhầm cần audit lại toàn bộ luồng thẻ + di chuyển dữ liệu/network — ước > 4 tuần, có rủi ro downtime |
### PA-2 — Payment Service là container riêng, network + IAM + RDS riêng hoàn toàn *(chọn)*
| | |
|---|---|
| **Mô tả** | Payment chạy trên ECS Fargate riêng, subnet/security group riêng, IAM role riêng, RDS riêng nhỏ — không service nào khác trong hệ thống truy cập trực tiếp RDS Payment; mọi module khác chỉ nhận `gateway_transaction_ref`/`status` qua API/event |
| **Ưu** | Phạm vi audit PCI-DSS SAQ A thu hẹp rõ ràng, kiểm chứng được bằng network diagram + IAM policy review; độc lập với `ADR-001` (áp dụng dù chọn P1 hay P2) |
| **Nhược** | Thêm một đơn vị triển khai riêng phải vận hành (đã tính vào `ADR-001`/`TCO`); mọi thay đổi cần gọi API/event thay vì truy cập DB trực tiếp — thêm độ trễ mạng cho luồng khởi tạo thanh toán |
| **Chi phí đảo ngược** | Thấp — đây là hướng thắt chặt hơn, gộp lại (nếu muốn) tốn ít hơn tách ra |
### PA-3 — Uỷ thác hoàn toàn cho VNPay/Momo (tokenization phía đối tác, sàn không giữ Payment Service riêng)
| | |
|---|---|
| **Mô tả** | Không xây Payment Service nội bộ — mọi trạng thái giao dịch tra cứu trực tiếp qua API đối tác mỗi khi cần, không lưu bản sao nào phía sàn |
| **Ưu** | Phạm vi PCI-DSS nhỏ nhất về lý thuyết |
| **Nhược** | Không đáp ứng `ASR-004` (đối soát bù cần log/trạng thái nội bộ để so khớp); phụ thuộc hoàn toàn uptime của đối tác cho mọi truy vấn trạng thái đơn hàng — vi phạm `QAS-002`/`QAS-014` |
| **Chi phí đảo ngược** | Cao — thiết kế lại toàn bộ luồng đối soát và trạng thái `Order`/`OrderSeller` nếu sau này cần Payment Service nội bộ |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Thu hẹp phạm vi PCI-DSS (`CON-06`) | ❌ Không kiểm chứng được | ✅ Rõ ràng | ✅ Nhỏ nhất về lý thuyết |
| Đáp ứng `ASR-004` (đối soát bù) | ✅ | ✅ | ❌ Không có log nội bộ |
| Chi phí vận hành thêm | Thấp | Trung bình | Thấp |
| Độ trễ khởi tạo thanh toán | Thấp nhất | Trung bình (gọi mạng) | Cao (phụ thuộc đối tác mỗi lần) |
## 3. Quyết định
> **Chọn PA-2 — Payment Service là container riêng, cô lập hoàn toàn network + IAM + dữ liệu.**
**Vì sao:** Đây là cách duy nhất trong 3 phương án vừa thu hẹp được phạm vi PCI-DSS SAQ A
(`CON-06`) vừa đáp ứng được yêu cầu đối soát bù (`ASR-004`). Độc lập với `ADR-001` — áp dụng bất
kể kết luận cuối cùng về kiểu kiến trúc tổng thể là gì.
**Phạm vi áp dụng:** Toàn bộ luồng xử lý thanh toán VNPay/Momo/COD, không có ngoại lệ.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Security xác nhận thiết kế cô lập (network diagram + IAM policy) đạt yêu cầu SAQ A | Security | Chưa có người Security thật (`OQ-004`) |
| Không bắt buộc POC (đây là ràng buộc pháp lý đã có sẵn, không phải con số hiệu năng chưa đo) | — | N/A |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Payment chung network/IAM với monolith | Không kiểm chứng được thu hẹp phạm vi PCI-DSS | `CON-06` | Không xét lại — vi phạm ràng buộc pháp lý cứng |
| PA-3 — Uỷ thác hoàn toàn cho đối tác, không Payment Service nội bộ | Không đáp ứng `ASR-004` (đối soát bù); phụ thuộc uptime đối tác cho mọi truy vấn | `ASR-004`, `QAS-002`, `QAS-014` | Chỉ xét lại nếu đối tác cung cấp webhook/API đối soát đủ tin cậy để không cần log nội bộ — chưa có bằng chứng |
## 5. Hệ quả
**Hệ quả tích cực**
- Phạm vi audit PCI-DSS SAQ A thu hẹp rõ ràng, kiểm chứng được bằng network diagram
- Payment Service có thể thay đổi độc lập (thêm kênh thanh toán mới) mà không chạm Nhóm Giao
dịch/Nhóm Hỗ trợ (`SAD §4.2` "Ranh giới thay đổi")
**Hệ quả tiêu cực phải sống chung**
- Mọi lời gọi từ Cart & Order tới Payment Service phải qua mạng (không còn truy vấn DB trực tiếp)
— thêm độ trễ và cần thiết kế đường lỗi riêng (xem `FAIL`, hoạt động 8)
- Thêm một RDS instance riêng cần patch/backup/monitor độc lập
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Gộp Payment vào chung network/IAM với phần còn lại | Phải làm lại toàn bộ audit PCI-DSS, rủi ro pháp lý cao |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Thiết kế network diagram + IAM policy chi tiết cho Security review | SA + Security | Hoạt động `sec` (GĐ2) | `SEC_e-commerce` §1 |
| Đào tạo team về ranh giới không được vi phạm (không copy code truy cập DB Payment) | Tech Lead | GĐ3 (`AGD`) | `TCO §5` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Không có security group nào cho phép truy cập trực tiếp từ container khác vào RDS Payment | Kiểm tra hạ tầng (Terraform plan/AWS Config rule) | CI/CD + định kỳ | `FIT-03` (ứng viên GĐ3) |
| Không có trường số thẻ/CVV trong bất kỳ schema nào ngoài Payment | Kiểm tra schema/migration review | CI (schema linter) | `FIT-04` (ứng viên) |
| Pentest/ASV scan hàng quý xác nhận phạm vi SAQ A | Nhà cung cấp pentest bên ngoài | Định kỳ hàng quý | — (thủ công, chưa có báo giá — `OQ-017`) |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Đối tác thanh toán mới yêu cầu mô hình tích hợp khác (không redirect/IPN) | Khi có đối tác mới | Tech Lead |
| Pentest/audit phát hiện lỗ hổng ở ranh giới cô lập | Bất kỳ phát hiện mức Cao | Security |
## 8. Tham chiếu
- POC: Không áp dụng (ràng buộc pháp lý, không phải quyết định hiệu năng)
- Bài đo: Pentest/ASV scan hàng quý (chưa có báo giá, `OQ-017`)
- Tài liệu ngoài: `BR_CartCheckout_v1.0.md` (BA) — `BR-CART-05` · `CTX_e-commerce_v1.0.md §3` `CON-06`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-002`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có người Security để ký (`ADL §8` việc #7) — ngoại lệ dự án chạy thử không thay được chữ ký chuyên môn Security |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,167 @@
# ADR-003 — Mô hình dữ liệu Order (cha) / OrderSeller (con) để tách đơn theo seller khi checkout
*Tên file: `adr/ADR-003_mo-hinh-order-orderseller-tach-don-theo-seller.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (cấu trúc dữ liệu — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~6/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-003` · `BR-CART-01` (BA) · `DRV-01` · `DRV-02` · `DRV-06` · `OQ-011` (BA, chưa đóng) · `CMP-04` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-003` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Giỏ hàng có thể chứa sản phẩm của nhiều seller. `DRV-01` (blocker MVP) và `DRV-06` (minh bạch
dòng tiền 3 bên khách–sàn–seller) buộc phải có một cách nhất quán để tách trách nhiệm tài chính
và vận chuyển giữa các seller trong cùng một lần đặt hàng. `BR-CART-01` (BA) đã phát biểu quy tắc
nghiệp vụ: nhóm `CartItem` theo `seller_id`, tạo `OrderSeller` riêng cho mỗi nhóm, `Order` là tập
hợp các `OrderSeller`. **Cơ chế tách chi tiết** ("luôn đúng N đơn cho N seller, không gộp") vẫn
phụ thuộc `OQ-011` (BA, `ASM-03` chưa xác minh) — ADR này ghi nhận mô hình cấu trúc, không tự
đóng `OQ-011`.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `BR-CART-01` (BA) | Tách `CartItem` theo `seller_id`, tạo `OrderSeller` riêng cho mỗi nhóm |
| `ASR-003` | Ép mô hình dữ liệu 1 `Order` — N `OrderSeller` — N `OrderItem`; mỗi `OrderSeller` có vòng đời trạng thái độc lập |
| `DRV-06` | Dòng tiền minh bạch 3 bên, hoa hồng/payout theo từng seller |
| `OQ-011` (BA) | Giỏ N seller luôn tách đúng N đơn, không gộp — **chưa xác nhận bởi PO** |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Mô hình kinh doanh là marketplace đa seller, cần tách trách nhiệm tài chính/vận chuyển theo seller | 🟢 Đã kiểm chứng | `00-project-brief.md` vòng 1, `BR-CART-01` |
| Không có ngoại lệ gộp nhiều seller vào 1 `OrderSeller` (VD cùng kho vận trung tâm) | 🔴 Giả định — PO chưa xác nhận | `OQ-011` (BA), `ASM-03` |
## 2. Phương án đã cân nhắc
### PA-1 — Một `Order` phẳng, không có cấu trúc con theo seller (dùng trường `seller_id` lặp ở từng `OrderItem`)
| | |
|---|---|
| **Mô tả** | Chỉ có bảng `Order`/`OrderItem`, mỗi `OrderItem` gắn `seller_id` riêng; không có tầng `OrderSeller` trung gian — trạng thái/hoa hồng tính bằng cách nhóm động theo `seller_id` mỗi lần truy vấn |
| **Ưu** | Đơn giản hơn về số bảng, ít JOIN hơn cho các truy vấn không cần phân seller |
| **Nhược** | Không có đơn vị hạch toán/vận chuyển/đối soát độc lập theo seller — mỗi lần cần trạng thái giao hàng hoặc hoa hồng theo seller phải nhóm động, dễ sai lệch khi có seller thay đổi trạng thái không đồng thời; vi phạm `ASR-003` ("mỗi `OrderSeller` có vòng đời trạng thái độc lập") |
| **Chi phí đảo ngược** | Cao — thêm tầng `OrderSeller` sau khi đã có dữ liệu lịch sử cần migrate toàn bộ đơn hàng cũ |
### PA-2 — `Order` (cha) — N `OrderSeller` (con) — N `OrderItem` *(chọn)*
| | |
|---|---|
| **Mô tả** | `Order` là tập hợp logic của các `OrderSeller`; mỗi `OrderSeller` có `status` riêng, thuộc về đúng 1 `Order` cha và đúng 1 `seller_id` |
| **Ưu** | Mỗi `OrderSeller` là đơn vị hạch toán/vận chuyển/đối soát độc lập — khớp trực tiếp `BR-CART-01`, `DRV-06`; các module tiêu thụ (Commission & Payout, Shipping & Fulfillment) chỉ cần đọc `OrderSeller`, không phải tự nhóm |
| **Nhược** | Thêm một tầng bảng, thêm JOIN khi cần tổng `Order` (đã có công thức `BR-CART-09` bù); ranh giới sở hữu dữ liệu giữa Cart & Order (tạo) và Commission/Shipping (tiêu thụ) cần rõ ràng |
| **Chi phí đảo ngược** | Trung bình-cao — đổi mô hình sau go-live cần migrate toàn bộ đơn hàng lịch sử + sửa mọi module đọc `Order` |
### PA-3 — Mỗi seller là một `Order` độc lập, không có `Order` cha chung
| | |
|---|---|
| **Mô tả** | Bỏ hẳn khái niệm "đơn cha" — khách checkout giỏ 3 seller thì tạo 3 `Order` độc lập hoàn toàn, không có liên kết logic nào |
| **Ưu** | Đơn giản nhất về mặt mô hình — mỗi `Order` tự chứa đủ thông tin |
| **Nhược** | Khách hàng mất khả năng xem "một lần đặt hàng" gồm nhiều seller như một đơn vị trải nghiệm (UX); mất điểm neo cho thanh toán chung 1 lần cho nhiều seller (khởi tạo Payment 1 lần cho N `OrderSeller`) — vi phạm luồng B5-B8 đã có ở `PROCESS_CartCheckout` |
| **Chi phí đảo ngược** | Cao — dựng lại khái niệm "đơn cha" sau này cần thêm bảng liên kết + backfill dữ liệu |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Đơn vị hạch toán/vận chuyển độc lập theo seller (`ASR-003`) | ❌ | ✅ | ✅ |
| Trải nghiệm "một lần đặt hàng" cho nhiều seller (`PROCESS` B5-B8) | 🔶 Nhóm động | ✅ | ❌ |
| Số bảng/độ phức tạp | Thấp | Trung bình | Thấp |
| Khớp `BR-CART-01` (BA) | ❌ | ✅ | 🔶 Một phần |
## 3. Quyết định
> **Chọn PA-2 — `Order` (cha) — N `OrderSeller` (con) — N `OrderItem`.**
**Vì sao:** Đây là mô hình duy nhất khớp trực tiếp `BR-CART-01` (nguồn nghiệp vụ đã chốt), đáp
ứng `ASR-003` (mỗi `OrderSeller` có vòng đời độc lập) và giữ được trải nghiệm "một lần đặt hàng"
theo luồng đã thiết kế ở `PROCESS_CartCheckout` B5-B8.
**Phạm vi áp dụng:** Toàn bộ luồng checkout và mọi module tiêu thụ `Order`/`OrderSeller`
(Commission & Payout, Shipping & Fulfillment, Review).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| `OQ-011` (BA) — PO xác nhận không có ngoại lệ gộp seller (VD cùng kho vận trung tâm) | PO | Mở |
| Không bắt buộc POC (đây là quyết định mô hình dữ liệu, không phải con số hiệu năng) | — | N/A |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — `Order` phẳng, không tầng `OrderSeller` | Vi phạm `ASR-003` (không có vòng đời độc lập theo seller); các module tiêu thụ phải tự nhóm động, dễ sai lệch | `ASR-003`, `BR-CART-01` | Nếu về sau xác nhận không cần vòng đời độc lập theo seller (VD gộp hết trách nhiệm về sàn) — chưa có dấu hiệu |
| PA-3 — Mỗi seller một `Order` độc lập, không cha chung | Phá vỡ trải nghiệm "một lần đặt hàng" đã thiết kế ở `PROCESS` B5-B8; mất điểm neo thanh toán chung | `PROCESS_CartCheckout` B5-B8 | Nếu PO đổi mô hình sản phẩm sang "mỗi seller là một cửa hàng độc lập hoàn toàn, khách checkout riêng từng seller" — thay đổi nghiệp vụ lớn, chưa có dấu hiệu |
## 5. Hệ quả
**Hệ quả tích cực**
- Commission & Payout, Shipping & Fulfillment chỉ cần đọc `OrderSeller`, không tự nhóm động —
giảm rủi ro sai lệch đối soát
- Khách hàng có trải nghiệm nhất quán "một lần đặt hàng" dù giỏ có nhiều seller
**Hệ quả tiêu cực phải sống chung**
- Thêm JOIN khi cần tổng `Order.total_amount` (đã có công thức bù `BR-CART-09`)
- Nếu `OQ-011` trả lời "có ngoại lệ gộp seller" (VD cùng kho vận), mô hình này phải viết lại một
phần (không phải toàn bộ — vẫn giữ cấu trúc cha-con, chỉ đổi quy tắc nhóm)
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi mô hình tách đơn (VD gộp nhiều seller vào 1 đơn) | Migrate toàn bộ đơn hàng lịch sử + sửa mọi module đọc `Order` — đây là core domain model, `SAD §4.2` đã cảnh báo đây là loại thay đổi "không nên xảy ra thường xuyên" |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| BA xác nhận `OQ-011` với PO | BA + PO | Trước khi `Accepted` | `ba-output/.../00-index/OQ_e-commerce.md` |
| Thiết kế schema chi tiết `Order`/`OrderSeller`/`OrderItem` | SA (hoạt động `dat`) | GĐ2 tiếp theo | `DAT_e-commerce` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Mọi `OrderSeller` luôn có đúng 1 `seller_id` và thuộc đúng 1 `Order` cha (ràng buộc FK + constraint) | DB schema constraint + migration test | CI | `FIT-05` (ứng viên) |
| Tổng `Order.total_amount` luôn bằng Σ `OrderSeller.subtotal_amount` (`BR-CART-09`) | Unit test / DB trigger kiểm tra | CI | `FIT-06` (ứng viên) |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| PO xác nhận cần ngoại lệ gộp seller (VD cùng kho vận trung tâm) | Khi `OQ-011` trả lời khác giả định hiện tại | PO + BA |
| Phát sinh nhu cầu đổi mô hình tách đơn thường xuyên | ≥ 2 lần yêu cầu thay đổi trong 6 tháng | Tech Lead |
## 8. Tham chiếu
- POC: Không áp dụng
- Bài đo: `FIT-05`, `FIT-06` (ứng viên GĐ3)
- Tài liệu ngoài: `BR_CartCheckout_v1.0.md §1 BR-CART-01, §4 ERD` (BA)
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-003`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chờ `OQ-011` — BA + PO xác nhận mô hình `Order`/`OrderSeller` |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,168 @@
# ADR-004 — Cơ chế idempotency cho webhook thanh toán + job đối soát bù định kỳ
*Tên file: `adr/ADR-004_idempotency-doi-soat-webhook-thanh-toan.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (tích hợp/idempotency — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~6/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-004` · `QAS-014` · `BR-CART-06` (BA) · `DRV-08` · `ARISK-03` · `CMP-11` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-004` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Xác nhận thanh toán qua webhook VNPay/Momo (bên thứ ba) chưa được xác minh khả thi gần thời gian
thực (`DRV-08`, `ASM-01` của BA chưa xác minh). Nếu webhook trễ/lỗi/gọi lặp, đơn hàng có thể kẹt
"chờ thanh toán" dù tiền đã thu, hoặc bị cập nhật trạng thái sai nếu xử lý webhook không
idempotent. `BR-CART-06` (BA) đã đặt quy tắc nghiệp vụ: chỉ cập nhật `Payment.status = success`
khi webhook có chữ ký hợp lệ và trong thời hạn — nhưng con số thời hạn cụ thể **chưa chốt**
(thuộc phạm vi kỹ thuật của ADR này, không phải quyết định BA).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-004` | Webhook phải idempotent + có job đối soát bù |
| `BR-CART-06` (BA) | Chỉ cập nhật `Payment.status=success` khi chữ ký hợp lệ + trong hạn |
| `QAS-014` | Job đối soát phát hiện lệch trong ≤15 phút *(đề xuất SA, `OQ-021` chưa xác nhận)* |
| `ARISK-03` | VNPay/Momo chưa có sandbox thật — thiết kế chưa kiểm chứng |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Webhook có thể bị gọi lặp lại (retry của VNPay/Momo theo tài liệu công khai) | 🟡 Ước lượng có cơ sở | Thực hành phổ biến của cổng thanh toán, chưa xác nhận với sandbox thật |
| `gateway_transaction_ref` là khoá duy nhất, ổn định qua các lần gọi lặp | 🔴 Giả định | Chưa có tài liệu API/sandbox thật (`CON-04`) |
| Tần suất đối soát ≤15 phút đủ nhanh để tránh khiếu nại CSKH | 🔴 Giả định (`ASM-18`) | `OQ-021` — Tech Lead chưa xác nhận |
## 2. Phương án đã cân nhắc
### PA-1 — Chỉ dựa vào webhook, không có job đối soát bù
| | |
|---|---|
| **Mô tả** | Cập nhật `Payment.status` hoàn toàn dựa vào webhook đến; không có cơ chế nào chủ động kiểm tra lại |
| **Ưu** | Đơn giản nhất, không cần job chạy định kỳ |
| **Nhược** | Nếu webhook mất/trễ vĩnh viễn (lỗi mạng, đối tác không gửi lại), đơn hàng kẹt "chờ thanh toán" vô thời hạn dù tiền đã thu — vi phạm trực tiếp `DRV-08`/`ASR-004` |
| **Chi phí đảo ngược** | Cao — phải xây job đối soát sau khi đã có dữ liệu thật bị kẹt, kèm backfill thủ công cho các đơn đã kẹt |
### PA-2 — Webhook idempotent (khoá theo `gateway_transaction_ref`) + job đối soát bù định kỳ *(chọn)*
| | |
|---|---|
| **Mô tả** | Webhook endpoint kiểm tra idempotency key = `gateway_transaction_ref` trước khi ghi; nếu đã xử lý, trả 200 mà không ghi lại. Job đối soát chạy định kỳ (đề xuất ≤15 phút, `OQ-021`), tra cứu API đối tác để so khớp `Payment.status` |
| **Ưu** | Chống hiệu ứng phụ khi webhook gọi lặp; phát hiện được các trường hợp webhook mất hoàn toàn (job chủ động tra cứu, không phụ thuộc webhook đến) |
| **Nhược** | Thêm một job chạy định kỳ cần giám sát riêng (alerting nếu job tự nó lỗi); cần bảng log webhook đã nhận để so khớp |
| **Chi phí đảo ngược** | Trung bình — đổi tần suất/cơ chế đối soát sau khi có dữ liệu thật tốn công viết lại job + backfill, nhưng không ảnh hưởng mô hình dữ liệu cốt lõi |
### PA-3 — Chỉ dùng job polling, không nhận webhook
| | |
|---|---|
| **Mô tả** | Bỏ hẳn endpoint webhook, chỉ dựa vào job định kỳ tra cứu API đối tác để cập nhật trạng thái |
| **Ưu** | Không cần lo idempotency cho webhook đến (không có webhook) |
| **Nhược** | Độ trễ xác nhận thanh toán phụ thuộc hoàn toàn chu kỳ polling (tối thiểu bằng tần suất job) — tệ hơn nhiều so với gần thời gian thực; tăng số lượng API call tới đối tác (chi phí + rủi ro rate limit) |
| **Chi phí đảo ngược** | Trung bình — thêm lại webhook sau này không khó, nhưng đã đánh đổi trải nghiệm người dùng trong thời gian dùng PA-3 |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Đáp ứng `ASR-004` (idempotent + đối soát bù) | ❌ | ✅ | 🔶 Có đối soát, không idempotent webhook (không có webhook) |
| Độ trễ xác nhận thanh toán (`QAS-002`) | Nhanh nhất khi webhook tới đúng hạn, nhưng vô hạn khi mất | Nhanh (webhook) + có lưới an toàn (job) | Chậm nhất (phụ thuộc chu kỳ polling) |
| Chi phí vận hành thêm | Thấp nhất | Trung bình (1 job định kỳ) | Trung bình-cao (nhiều API call hơn) |
## 3. Quyết định
> **Chọn PA-2 — Webhook idempotent (khoá `gateway_transaction_ref`) + job đối soát bù định kỳ.**
**Vì sao:** Đây là phương án duy nhất vừa giữ được độ trễ thấp cho trường hợp bình thường (webhook
tới đúng hạn) vừa có lưới an toàn khi webhook trễ/mất — đúng yêu cầu `ASR-004`.
**Phạm vi áp dụng:** Toàn bộ luồng xác nhận thanh toán VNPay/Momo (không áp dụng cho COD — không
có webhook).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Có sandbox VNPay/Momo thật để kiểm chứng hành vi retry webhook | Tech Lead | Chưa có (`ARISK-03`, `CON-04`) |
| `OQ-021` — Tech Lead xác nhận tần suất job đối soát cụ thể | Tech Lead | Mở (đề xuất tạm ≤15 phút) |
| Không bắt buộc POC theo `decision-radar.md §6` (không phải con số hiệu năng lớn, nhưng cần sandbox thật để xác minh hành vi đối tác) | — | Chờ sandbox |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Chỉ dựa vào webhook, không đối soát | Không phát hiện được trường hợp webhook mất hoàn toàn — vi phạm `ASR-004` | `ASR-004`, `DRV-08` | Không xét lại trừ khi VNPay/Momo đảm bảo bằng SLA hợp đồng webhook không bao giờ mất — chưa có tiền lệ ngành |
| PA-3 — Chỉ polling, không webhook | Độ trễ xác nhận kém hơn hẳn, tăng API call — vi phạm gián tiếp `QAS-002` (trải nghiệm checkout) | `QAS-002`, `QAS-014` | Nếu VNPay/Momo không hỗ trợ webhook (một số cổng thanh toán khác trong tương lai) — xét lại cho riêng đối tác đó |
## 5. Hệ quả
**Hệ quả tích cực**
- Chống được hiệu ứng phụ khi webhook gọi lặp (idempotency key)
- Có lưới an toàn phát hiện đơn kẹt trong thời gian giới hạn (đề xuất ≤15 phút), giảm rủi ro
khiếu nại CSKH (`DRV-08`)
**Hệ quả tiêu cực phải sống chung**
- Cần bảng log webhook đã nhận (`ReconciliationLog`) — tăng dung lượng lưu trữ, cần retention
policy (thuộc `DAT`, hoạt động 5)
- Job đối soát tự nó có thể lỗi — cần alerting riêng cho chính job này (không chỉ alerting cho
luồng chính)
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi khoá idempotency từ `gateway_transaction_ref` sang cơ chế khác | Cần migrate log webhook đã có + rà lại toàn bộ giao dịch lịch sử |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Đàm phán sandbox VNPay/Momo | Tech Lead + PO | Trước GĐ2 `ICD` hoàn tất | `ARISK-03` |
| Thiết kế bảng `ReconciliationLog` + retention | SA (hoạt động `dat`) | GĐ2 tiếp theo | `DAT_e-commerce` |
| Thêm alerting cho chính job đối soát (job lỗi/không chạy) | SA (hoạt động `inf`) | GĐ2 tiếp theo | `INF_e-commerce` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Gọi lặp cùng một webhook payload không tạo hiệu ứng phụ (không nhân đôi trạng thái/log) | Integration test | CI + staging trước mỗi release | `FIT-07` (ứng viên) |
| Job đối soát phát hiện lệch giả lập trong ≤15 phút | Test giả lập webhook trễ/mất trên staging | Trước go-live | — (cần sandbox thật, `ARISK-03`) |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Tần suất job đối soát thật khác đề xuất (`OQ-021` trả lời khác ≤15 phút) | Khi Tech Lead xác nhận số khác | Tech Lead |
| Sandbox thật cho thấy webhook có hành vi khác giả định (VD không gọi lặp, hoặc gọi lặp với `gateway_transaction_ref` khác nhau) | Khi có sandbox | Tech Lead |
## 8. Tham chiếu
- POC: Không bắt buộc — nhưng cần sandbox thật (`ARISK-03`) trước khi kiểm chứng đầy đủ
- Bài đo: `FIT-07` (ứng viên GĐ3)
- Tài liệu ngoài: `BR_CartCheckout_v1.0.md §1 BR-CART-06` (BA)
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-004`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có chữ ký thật Tech Lead; cần chạy thử trên sandbox đối tác thanh toán trước khi chốt |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,176 @@
# ADR-005 — Message backbone cho luồng đặt hàng là SQS FIFO (per-seller group) + EventBridge, không phải Kafka/MSK
*Tên file: `adr/ADR-005_message-backbone-sqs-fifo-eventbridge.md`*
| | |
|---|---|
| **Status** | `Proposed` *(🔴 radar ≥8 — theo `decision-radar.md §2`/§6, **không được chuyển `Accepted` khi chưa có `POC-01` PASS**)* |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (tích hợp/hạ tầng — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~8/10** — 🔴 bắt buộc POC trước `Accepted` |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-005` · `QAS-005` · `QAS-009` (A4 xung đột dòng 2) · `ARISK-01` · `POC-01` · `ADR-001` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-005` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Luồng sự kiện `OrderPlaced`/`PaymentConfirmed` phải truyền tới ≥5 consumer (Commission & Payout,
Promotion & Loyalty, Review, Notification, Shipping & Fulfillment) sau khi đặt hàng, đồng thời
phải giữ đúng thứ tự xử lý trong cùng một seller (liên quan `DRV-06` — tính nhất quán dữ liệu tài
chính). `ASR-001`/`ADR-001` đã chọn hướng managed service (SQS/EventBridge) thay vì tự vận hành
Kafka/MSK — ADR này hình thức hoá cụ thể lựa chọn message backbone và **là quyết định đạt ngưỡng
radar ≥8** vì chưa có bài đo throughput thật trong ngữ cảnh này (`ASM-07`).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-005` | SQS FIFO (message group = `seller_id`) + EventBridge fan-out ≥5 consumer |
| `QAS-005` | Thông lượng sustained ≥100 msg/s, p99 publish→consume ≤2s, 0 mất message, fan-out lag ≤5s |
| `QAS-009` A4 dòng 2 | Xung đột: AWS giới hạn throughput **cứng cho một message group** SQS FIFO — seller volume cao có thể chạm trần group dù tổng hệ thống chưa chạm 100 msg/s |
| `CON-05` | Team chưa xác nhận kinh nghiệm vận hành Kafka/MSK production (`CTX §4.4`) |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| SQS FIFO hỗ trợ message group theo key tuỳ chọn (`seller_id`) | 🟢 Đã kiểm chứng | Tài liệu AWS công khai |
| SQS FIFO + EventBridge đạt ≥100 msg/s sustained, p99 ≤2s trong ngữ cảnh payload `OrderPlaced` ~2KB tại `ap-southeast-1` | 🔴 Giả định (`ASM-07`) | `POC-01` — **chưa chạy** |
| Một seller volume rất cao không làm nghẽn message group riêng của seller đó tới mức ảnh hưởng SLA | 🔴 Giả định | Chưa có POC riêng cho kịch bản seller lệch tải — ghi nhận ở §7 |
## 2. Phương án đã cân nhắc
### PA-1 — Kafka / Amazon MSK tự quản lý
| | |
|---|---|
| **Mô tả** | Cụm Kafka/MSK làm xương sống bất đồng bộ, theo `SAD.md`/hồ sơ thầu (P1) |
| **Ưu** | Throughput cao, kiểm soát partition/consumer group linh hoạt hơn; công nghệ phổ biến cho hệ thống lớn |
| **Nhược** | Không có bằng chứng team từng vận hành Kafka/MSK production (`CTX §4.4`, `OQ-010` mở); effort dựng cụm riêng đã ước 70,2 MD (XL, +35% contingency, `WBS-03`); thêm một cụm phải patch/scale thủ công |
| **Chi phí đảo ngược** | Cao — bỏ Kafka/MSK sau khi đã có producer/consumer thật cần viết lại toàn bộ tầng tích hợp bất đồng bộ |
### PA-2 — SQS Standard + SNS fan-out (không FIFO)
| | |
|---|---|
| **Mô tả** | Dùng SQS Standard (at-least-once, không đảm bảo thứ tự) + SNS để fan-out cho nhiều consumer |
| **Ưu** | Throughput lý thuyết cao hơn FIFO (không giới hạn theo message group), chi phí thấp |
| **Nhược** | **Không đảm bảo thứ tự xử lý trong cùng seller** — vi phạm trực tiếp yêu cầu tính nhất quán tài chính của `DRV-06` (VD 2 sự kiện `OrderPlaced`/`PaymentConfirmed` cùng seller có thể xử lý sai thứ tự, gây sai lệch đối soát) |
| **Chi phí đảo ngược** | Trung bình — chuyển sang FIFO sau này cần viết lại consumer để xử lý idempotent/thứ tự, nhưng không cần đổi hạ tầng lớn |
### PA-3 — SQS FIFO (per-seller message group) + EventBridge fan-out *(chọn)*
| | |
|---|---|
| **Mô tả** | SQS FIFO với message group key = `seller_id` (đảm bảo thứ tự trong cùng seller), EventBridge rule fan-out tới ≥5 consumer |
| **Ưu** | Managed hoàn toàn (không cụm phải patch/scale thủ công); đảm bảo thứ tự đúng phạm vi cần (trong seller, không cần thứ tự toàn cục); khớp năng lực team hiện tại (không cần kinh nghiệm Kafka) |
| **Nhược** | Giới hạn throughput cứng theo **một** message group — seller volume rất cao có thể chạm trần riêng của seller đó (`QAS-009` A4); FIFO có overhead cao hơn Standard |
| **Chi phí đảo ngược** | Trung bình — nếu cần chuyển sang Kafka/MSK sau này (do `POC-01` fail hoặc traffic vượt xa dự kiến), phải viết lại toàn bộ tầng publish/consume |
### Bảng so sánh
| Tiêu chí | PA-1 (Kafka/MSK) | PA-2 (SQS Standard+SNS) | PA-3 (SQS FIFO+EventBridge) |
|---|---|---|---|
| Đảm bảo thứ tự trong seller (`DRV-06`) | ✅ (tự cấu hình partition key) | ❌ | ✅ |
| Rủi ro năng lực team (`CON-05`) | 🔴 Cao — chưa có kinh nghiệm | 🟢 Thấp | 🟢 Thấp |
| Chi phí vận hành (cụm tự quản lý) | 🔴 Cao | 🟢 Thấp | 🟢 Thấp |
| Rủi ro trần throughput 1 seller lớn | 🟢 Thấp (partition linh hoạt) | 🟢 Thấp | 🟠 Có (per-group limit) |
## 3. Quyết định
> **Chọn PA-3 — SQS FIFO (message group = `seller_id`) + EventBridge fan-out ≥5 consumer.**
**Vì sao:** Đây là phương án duy nhất vừa đảm bảo thứ tự trong phạm vi cần (theo seller, khớp
`DRV-06`) vừa khớp năng lực team hiện tại (`CON-05`, không cần vận hành cụm Kafka/MSK) — nhất
quán với hướng đi đã chọn ở `ADR-001`.
**Phạm vi áp dụng:** Toàn bộ luồng sự kiện `OrderPlaced`/`PaymentConfirmed` và các sự kiện tương
tự phát sinh sau đặt hàng.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| **`POC-01` PASS** — sustained ≥100 msg/s/30 phút, p99 ≤2s, 0 mất message, fan-out lag ≤5s | Tech Lead + 1 DevOps | 🔴 **Chưa chạy** (`ARISK-01`, `OQ-012`) |
| Có phương án theo dõi/cảnh báo riêng cho seller volume cao (giảm rủi ro trần message group) | Tech Lead | Chưa thiết kế — xem §5 "Việc phát sinh" |
🔴 **Bắt buộc theo `decision-radar.md §2`/§6** — điểm radar ~8 (≥8) đồng nghĩa ADR này **không
được chuyển `Accepted`** cho tới khi `POC-01` chạy xong và PASS. Nếu `POC-01` FAIL: bổ sung
Kafka/MSK riêng cho hot-path đặt hàng (kiến trúc lai, theo điều kiện đã nêu ở `OPT §1`), không
Supersede toàn bộ `ADR-001`.
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Kafka/MSK | Không có bằng chứng team vận hành production; effort dựng nền tảng lớn hơn đáng kể | `CON-05`, `OQ-010` | Nếu `POC-01` FAIL **và** `OQ-010` xác nhận team có kinh nghiệm Kafka/MSK — bổ sung Kafka/MSK riêng cho hot-path (kiến trúc lai), không thay thế toàn bộ |
| PA-2 — SQS Standard + SNS | Không đảm bảo thứ tự trong seller — vi phạm `DRV-06` | `DRV-06` | Chỉ xét lại cho các luồng không yêu cầu thứ tự (VD Notification không cần đúng thứ tự tuyệt đối) — không áp dụng cho `OrderPlaced`/`PaymentConfirmed` |
## 5. Hệ quả
**Hệ quả tích cực**
- Không cần vận hành cụm message broker riêng — khớp năng lực team, giảm effort nền tảng ~70,2 MD
so với Kafka/MSK
- Đảm bảo đúng thứ tự xử lý trong phạm vi cần (theo seller)
**Hệ quả tiêu cực phải sống chung**
- Rủi ro trần throughput cho **một** seller có volume rất cao — cần theo dõi/cảnh báo riêng
(chưa thiết kế, xem việc phát sinh)
- Nếu `POC-01` FAIL, phải bổ sung kiến trúc lai (Kafka/MSK cho riêng hot-path) — tăng độ phức tạp
vận hành so với một giải pháp thuần nhất
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Chuyển từ SQS FIFO/EventBridge sang Kafka/MSK sau khi đã có producer/consumer thật | Viết lại toàn bộ tầng publish/consume, thiết kế lại partition/consumer group |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Chạy `POC-01` theo tiêu chí đã viết trước | Tech Lead + DevOps | Trước khi `Accepted` | `ARISK_e-commerce_v1.0.md §6` |
| Thiết kế cơ chế theo dõi/cảnh báo riêng cho seller volume cao (VD CloudWatch alarm theo group lag) | SA (hoạt động `inf`) + Tech Lead | GĐ2 tiếp theo | `INF_e-commerce` |
| Thêm fitness function kiểm tra publish luôn có `seller_id` làm message group key | Tech Lead | GĐ3 (`AGD`/`FIT`) | `FIT-08` (ứng viên) |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| `POC-01` — throughput/độ trễ/mất message | k6/artillery, AWS `ap-southeast-1` thật | Trước `Accepted`, lặp lại trước mỗi major release | — (ghi vào `ARISK §6`) |
| Mọi message publish `OrderPlaced`/`PaymentConfirmed` có `MessageGroupId = seller_id` | Contract test / lint publish code | CI | `FIT-08` (ứng viên) |
| Cảnh báo khi một message group riêng lẻ có lag > ngưỡng | CloudWatch Alarm | Production, liên tục | — |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| `POC-01` FAIL | Không đạt tiêu chí PASS đã viết trước | Tech Lead |
| Một seller có volume vượt trần message group SQS FIFO trong production | Lag message group > 5 giây kéo dài > 5 phút liên tục | SRE |
| Tổng throughput hệ thống tiệm cận giới hạn đã đo ở `POC-01` | > 80% ngưỡng đã đo | SRE |
## 8. Tham chiếu
- POC: `ARISK_e-commerce_v1.0.md §6 POC-01`
- Bài đo: `FIT-08` (ứng viên GĐ3)
- Tài liệu ngoài: `QAS_e-commerce_v1.0.md §A3 QAS-005, §A4 (xung đột dòng 2)`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-005`, `OPT_e-commerce_v1.0.md §8 ASM-07`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Radar 8 — bắt buộc `POC-01` (throughput SQS/EventBridge) PASS trước khi `Accepted`; chưa chạy |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,174 @@
# ADR-006 — Một cụm RDS PostgreSQL chính, schema-per-module, chịu tải ghi/đọc gộp (trừ Payment)
*Tên file: `adr/ADR-006_chien-luoc-du-lieu-rds-gop-schema-per-module.md`*
| | |
|---|---|
| **Status** | `Proposed` *(🔴 radar ≥8 — theo `decision-radar.md §2`/§6, **không được chuyển `Accepted` khi chưa có `POC-02` PASS**)* |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead/DBA (dữ liệu — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~8/10** — 🔴 bắt buộc POC trước `Accepted` |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-006` · `QAS-006` · `ARISK-02` · `POC-02` · `ADR-001` · `ADR-002` (Payment tách riêng, độc lập) |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-006` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Modular monolith P2 (`ADR-001`) cần một chiến lược dữ liệu nhất quán cho mọi module ngoài
Payment (đã tách riêng theo `ADR-002`). `ASR-006` ép ra một cụm RDS PostgreSQL Multi-AZ chính,
chia theo schema-per-module — chưa đo tải ghi (checkout) + đọc (catalog) gộp ở tải đỉnh flash
sale. Đây là quyết định đạt ngưỡng radar ≥8 vì chưa có bài đo thật trong ngữ cảnh này (`ASM-08`).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-006` | 1 RDS chính schema-per-module, chịu tải ghi/đọc gộp cho mọi module trừ Payment |
| `QAS-006` | p95 write (checkout) ≤300ms, p95 read (catalog) ≤200ms, CPU <75%, connection pool không exhaust |
| `ADR-001` | Modular monolith P2 — tiền đề cho việc gộp dữ liệu theo schema thay vì database-per-service |
| `ADR-002` | Payment có RDS riêng — không nằm trong phạm vi cụm RDS chính này |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Payment có RDS riêng, tách biệt hoàn toàn khỏi cụm chính | 🟢 Đã kiểm chứng | `ADR-002` |
| 1 cụm RDS `db.r6g.xlarge` Multi-AZ đủ chịu tải ghi/đọc gộp cho các module ngoài Payment ở kịch bản A/B | 🔴 Giả định (`ASM-08`) | `POC-02` — **chưa chạy** |
| Connection pool chia theo module đủ tránh một module chiếm hết pool | 🔴 Giả định | Chưa có thiết kế connection pool chi tiết — thuộc `DAT`, hoạt động 5 |
## 2. Phương án đã cân nhắc
### PA-1 — Database-per-service (mỗi module 1 RDS logic riêng)
| | |
|---|---|
| **Mô tả** | Mỗi module (Identity, Catalog, Cart & Order, Seller, Commission, Promotion, Review, Notification, Shipping) có 1 RDS instance/logic DB riêng — theo `SAD.md`/hồ sơ thầu (P1) |
| **Ưu** | Cô lập tải hoàn toàn — một module quá tải không ảnh hưởng module khác; dễ scale/tune riêng từng DB |
| **Nhược** | ~9-10 RDS instance phải patch/backup/monitor độc lập — vi phạm Conway với năng lực team hiện tại (`CON-05`); tăng chi phí vận hành đáng kể so với 1-2 instance |
| **Chi phí đảo ngược** | Cao — gộp lại thành 1 cụm sau này cần migrate dữ liệu qua nhiều DB, đồng bộ schema |
### PA-2 — 1 cụm RDS chính, schema-per-module (ranh giới logic, chung instance) *(chọn)*
| | |
|---|---|
| **Mô tả** | 1 cụm RDS PostgreSQL Multi-AZ, mỗi module có schema riêng trong cùng instance (ranh giới logic rõ ràng qua schema + quyền truy cập DB user riêng theo schema) |
| **Ưu** | Ít thành phần phải vận hành hơn (1 instance thay vì ~9-10) — khớp Conway với team hiện tại; vẫn giữ ranh giới logic rõ ràng qua schema (không phải 1 schema dùng chung hỗn loạn) |
| **Nhược** | Một module quá tải (VD Catalog bị truy vấn nặng mùa flash sale) có thể ảnh hưởng module khác dùng chung instance (Cart & Order, Commission…) — chưa đo tải gộp thật |
| **Chi phí đảo ngược** | Trung bình-cao — tách read replica/DB riêng cho từng module sau khi đã gộp là một dự án migration riêng |
### PA-3 — 1 cụm RDS, 1 schema chung (không phân tách theo module)
| | |
|---|---|
| **Mô tả** | Tất cả bảng của mọi module nằm chung 1 schema, không phân tách quyền truy cập theo module |
| **Ưu** | Đơn giản nhất về mặt cấu hình DB ban đầu |
| **Nhược** | Không có ranh giới logic nào giữa các module — vi phạm `D7` ("mỗi thực thể có đúng một chủ sở hữu" cần ranh giới rõ để enforce); dễ phát sinh coupling ngầm (module A viết trực tiếp bảng của module B) làm mất tác dụng modular monolith |
| **Chi phí đảo ngược** | Cao — tách schema sau khi code đã coupling chéo là công việc tái cấu trúc lớn |
### Bảng so sánh
| Tiêu chí | PA-1 (DB-per-service) | PA-2 (1 cụm, schema-per-module) | PA-3 (1 cụm, 1 schema) |
|---|---|---|---|
| Số thành phần phải vận hành | ~9-10 | 1 | 1 |
| Cô lập tải giữa module | ✅ Hoàn toàn | 🟠 Một phần (chung instance) | ❌ Không |
| Ranh giới logic rõ ràng (`D7`) | ✅ | ✅ (qua schema) | ❌ |
| Khớp Conway (`CON-05`) | ❌ | ✅ | ✅ |
## 3. Quyết định
> **Chọn PA-2 — 1 cụm RDS PostgreSQL Multi-AZ chính, schema-per-module, cho mọi module ngoại trừ
> Payment.**
**Vì sao:** Cân bằng giữa khớp Conway (`CON-05`, ít thành phần vận hành hơn database-per-service)
và vẫn giữ ranh giới logic rõ ràng qua schema (khác PA-3 gộp chung 1 schema mất kiểm soát ranh
giới dữ liệu).
**Phạm vi áp dụng:** Toàn bộ module ngoại trừ Payment (đã có RDS riêng theo `ADR-002`).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| **`POC-02` PASS** — p95 write ≤300ms, p95 read ≤200ms, CPU <75%, connection pool không exhaust | Tech Lead/DBA | 🔴 **Chưa chạy** (`ARISK-02`) |
| Thiết kế connection pool theo module (giới hạn để tránh một module chiếm hết pool) | Tech Lead/DBA | Chưa thiết kế — thuộc `DAT` |
🔴 **Bắt buộc theo `decision-radar.md §2`/§6** — điểm radar ~8 (≥8) đồng nghĩa ADR này **không
được chuyển `Accepted`** cho tới khi `POC-02` chạy xong và PASS. Nếu `POC-02` FAIL: tách read
replica/DB riêng cho Catalog/Search sớm hơn dự kiến, cập nhật `TCO` (chi phí hạ tầng tăng, tiệm
cận P1 cho phần đó — đã cảnh báo ở `TCO §3.3`).
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Database-per-service | Vi phạm Conway với năng lực team hiện tại; tăng chi phí vận hành đáng kể | `CON-05`, `OPT §4` tiêu chí 5 | Nếu `POC-02` FAIL nghiêm trọng (không thể khắc phục bằng read replica) **và** team đủ lớn để vận hành nhiều DB — xét lại cho riêng module gây nghẽn (VD tách Catalog) |
| PA-3 — 1 cụm, 1 schema chung | Không có ranh giới logic — vi phạm `D7`, dễ coupling chéo giữa module | `D7` (design-rules) | Không xét lại — đây là anti-pattern cho modular monolith, không phải trade-off hợp lý |
## 5. Hệ quả
**Hệ quả tích cực**
- Chỉ 1 RDS instance chính phải patch/backup/monitor (ngoài Payment) — khớp năng lực team, giảm
chi phí vận hành đáng kể so với database-per-service
- Ranh giới logic rõ ràng qua schema — enforce được `D7` (mỗi thực thể có đúng một chủ)
**Hệ quả tiêu cực phải sống chung**
- Một module quá tải (VD Catalog mùa flash sale) có thể ảnh hưởng module khác dùng chung instance
— cần connection pool giới hạn theo module để giảm thiểu
- Nếu `POC-02` FAIL, phải tách read replica sớm hơn dự kiến — tăng chi phí hạ tầng ngoài kế hoạch
ban đầu
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Tách một module ra RDS riêng sau khi đã gộp | Migration dữ liệu + đồng bộ song song trong thời gian chuyển đổi — dự án riêng, không miễn phí |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Chạy `POC-02` theo tiêu chí đã viết trước | Tech Lead/DBA | Trước khi `Accepted` | `ARISK_e-commerce_v1.0.md §6` |
| Thiết kế connection pool giới hạn theo module | SA (hoạt động `dat`) + DBA | GĐ2 tiếp theo | `DAT_e-commerce` |
| Kế hoạch tách read replica dự phòng nếu `POC-02` FAIL | Tech Lead/DBA | Khi có kết quả `POC-02` | `TDEBT` (GĐ3) hoặc cập nhật `TCO` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| `POC-02` — tải ghi/đọc gộp | Load test staging, cấu hình instance thật | Trước `Accepted`, lặp lại trước mỗi major release | — (ghi vào `ARISK §6`) |
| Không có bảng nào bị truy cập chéo schema (module A viết trực tiếp bảng module B) | DB user permission theo schema + code review/lint | CI (schema access lint) | `FIT-09` (ứng viên) |
| CPU/connection pool RDS trong ngưỡng ở production | CloudWatch metric + alarm | Production, liên tục | — |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| `POC-02` FAIL | Không đạt tiêu chí PASS đã viết trước | Tech Lead/DBA |
| CPU RDS production vượt 75% thường xuyên | > 3 lần/tuần trong giờ cao điểm | SRE |
| Connection pool exhaust trong production | Bất kỳ lần nào | SRE |
## 8. Tham chiếu
- POC: `ARISK_e-commerce_v1.0.md §6 POC-02`
- Bài đo: `FIT-09` (ứng viên GĐ3)
- Tài liệu ngoài: `QAS_e-commerce_v1.0.md §A3 QAS-006`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-006`, `OPT_e-commerce_v1.0.md §8 ASM-08`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Radar 8 — bắt buộc `POC-02` (tải RDS gộp) PASS trước khi `Accepted`; chưa chạy |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,171 @@
# ADR-007 — Chỗ ra quyết định ownership/authz cho Cart & Order là service (có cache Redis), không phải gateway
*Tên file: `adr/ADR-007_cho-quyet-dinh-ownership-authz-cart-order.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (đề xuất SA, chốt SA+Tech Lead theo `decision-radar.md §5`; Security có quyền phủ quyết vì đây là quyết định authz) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~6/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-007` · `QAS-010` · `QAS-003`/`QAS-004` (A4 xung đột dòng 1) · `RBAC_CartCheckout` (BA) §2 · `CMP-02`, `CMP-04`, `CMP-15` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-007` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Mọi thao tác đọc/sửa/xoá `Cart`/`CartItem`/`Order` phải được đối chiếu quyền sở hữu: Guest theo
`session_id` của chính phiên, Customer theo `customer_id` của chính họ — 0% truy cập chéo (IDOR)
thành công (`ASR-007`, `QAS-010`). `RBAC_CartCheckout_v1.0.md §2` (BA) đã xác nhận ma trận: cả
Guest và Customer đều **không** được xem giỏ hàng/đơn hàng của người khác. Vấn đề kỹ thuật cần
chốt: **kiểm tra ownership ở đâu** (gateway hay service) và **có cache không** — vì kiểm tra mỗi
request cạnh tranh trực tiếp với ngân sách latency của `QAS-003`/`QAS-004` (đã ghi nhận xung đột
ở `QAS §A4` dòng 1).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-007` | 100% request qua kiểm tra ownership, 0% IDOR thành công |
| `QAS-010` | Test tự động xác nhận 0 lượt IDOR thành công |
| `QAS-003`/`QAS-004` | p95 ≤500ms (`GET /v1/cart`), p95 ≤300ms (`PATCH`/`DELETE /v1/cart/items`) — 🔴 số SA đề xuất, `OQ-018`/`OQ-019` chưa xác nhận |
| `RBAC_CartCheckout §2` (BA) | Guest theo `session_id`, Customer theo `customer_id` — ma trận đã xác nhận |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Guest/Customer không được xem giỏ/đơn của người khác | 🟢 Đã kiểm chứng | `RBAC_CartCheckout §2` (BA), đã duyệt từng phần |
| Query DB mỗi request để kiểm tra ownership sẽ vi phạm ngân sách latency `QAS-003`/`QAS-004` | 🟡 Ước lượng có cơ sở | Chưa có bài đo thật — cần đo sau khi có thiết kế cụ thể (`QAS §A4`) |
| Redis cache session/ownership đủ nhanh để giữ latency trong ngân sách | 🔴 Giả định | Chưa có POC riêng cho cache ownership |
## 2. Phương án đã cân nhắc
### PA-1 — Kiểm tra ownership tại API Gateway/ALB (trước khi vào service)
| | |
|---|---|
| **Mô tả** | Gateway giải mã JWT/session, đối chiếu ownership ngay tại lớp gateway trước khi forward request vào Cart & Order |
| **Ưu** | Chặn sớm request không hợp lệ, giảm tải cho service phía sau |
| **Nhược** | Gateway (ALB/API Gateway managed) không có quyền truy cập trực tiếp dữ liệu nghiệp vụ (`CartItem` thuộc về ai) — phải gọi ngược lại service hoặc DB để biết ownership, mất lợi thế "chặn sớm"; đặt business logic vào lớp hạ tầng dùng chung vi phạm ranh giới "cái CMP-01 KHÔNG làm" đã ghi ở `SAD §4.1` |
| **Chi phí đảo ngược** | Trung bình — chuyển logic xuống service sau này cần tách lại code đã đặt sai lớp |
### PA-2 — Kiểm tra ownership tại service (Cart & Order), query DB mỗi request
| | |
|---|---|
| **Mô tả** | Service tự đối chiếu `session_id`/`customer_id` với DB mỗi request, không cache |
| **Ưu** | Đơn giản, luôn chính xác 100% (không có dữ liệu cache cũ) |
| **Nhược** | Thêm 1 round-trip DB cho mỗi request — cạnh tranh trực tiếp với ngân sách latency `QAS-003`/`QAS-004` (đã ghi xung đột `QAS §A4`) |
| **Chi phí đảo ngược** | Thấp — thêm cache sau này là cải tiến, không phải viết lại |
### PA-3 — Kiểm tra ownership tại service, cache session/ownership trong Redis *(chọn)*
| | |
|---|---|
| **Mô tả** | Service tự đối chiếu ownership; thông tin session/ownership được cache trong Redis (TTL ngắn), chỉ query DB khi cache miss |
| **Ưu** | Giữ ranh giới đúng (authz logic ở service sở hữu dữ liệu, không phải gateway); giảm round-trip DB cho phần lớn request — cân bằng được `ASR-007` và `QAS-003`/`004` |
| **Nhược** | Thêm thành phần cache cần vận hành (đã có ElastiCache Redis trong kiến trúc — `CMP-15`); cần xử lý đúng khi cache invalidate (VD khi Guest session hết hạn, khi Customer đổi giỏ hàng) |
| **Chi phí đảo ngược** | Thấp-trung bình — bỏ cache (quay về PA-2) nếu phát hiện vấn đề nhất quán, không cần viết lại toàn bộ |
### Bảng so sánh
| Tiêu chí | PA-1 (Gateway) | PA-2 (Service, không cache) | PA-3 (Service + Redis cache) |
|---|---|---|---|
| Đúng ranh giới sở hữu logic (`SAD §4.1` "cái CMP-01 KHÔNG làm") | ❌ | ✅ | ✅ |
| Đáp ứng ngân sách latency `QAS-003`/`004` | 🔶 Chưa rõ (vẫn cần gọi ngược) | ❌ Rủi ro cao | ✅ (kỳ vọng, chưa đo) |
| Độ phức tạp vận hành thêm | Thấp | Thấp nhất | Trung bình (thêm cache) |
| Đảm bảo 0% IDOR (`ASR-007`) | ✅ (nếu gọi ngược đúng) | ✅ | ✅ (cần xử lý invalidate đúng) |
## 3. Quyết định
> **Chọn PA-3 — Kiểm tra ownership tại service (Cart & Order), cache session/ownership trong
> Redis, query DB khi cache miss.**
**Vì sao:** Đây là phương án duy nhất giữ đúng ranh giới sở hữu logic (`CMP-01` API
Gateway/ALB đã ghi rõ "không tự quyết định authz chi tiết" ở `SAD §4.1`) trong khi vẫn có cơ hội
đáp ứng ngân sách latency `QAS-003`/`004` tốt hơn PA-2.
**Phạm vi áp dụng:** Toàn bộ endpoint `GET`/`PATCH`/`DELETE /v1/cart*` và các endpoint liên quan
`Order` trong phạm vi Cart & Order Service.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Đo lại `QAS-003`/`QAS-004` sau khi có thiết kế cache cụ thể (theo `QAS §A4` đã ghi) | QA + Tech Lead | Chưa đo |
| Thiết kế cơ chế invalidate cache đúng khi session hết hạn/giỏ hàng đổi | Tech Lead | Chưa thiết kế |
| Không bắt buộc POC riêng (radar 6, dưới ngưỡng 8) nhưng khuyến nghị đo trước khi `Accepted` | Tech Lead | — |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Kiểm tra tại Gateway | Gateway không có quyền truy cập dữ liệu nghiệp vụ, đặt sai ranh giới logic | `SAD §4.1` cột "cái nó KHÔNG làm" của `CMP-01` | Nếu về sau có một BFF (Backend-for-Frontend) layer sở hữu session tập trung — xét lại cho riêng phần xác thực (không phải authz chi tiết theo dữ liệu) |
| PA-2 — Service, không cache | Rủi ro vi phạm ngân sách latency `QAS-003`/`004` do round-trip DB mỗi request | `QAS-003`, `QAS-004`, `QAS §A4` | Nếu đo thực tế cho thấy query DB đơn giản (index tốt) đủ nhanh mà không cần cache — đơn giản hoá bằng cách bỏ Redis, giảm 1 thành phần vận hành |
## 5. Hệ quả
**Hệ quả tích cực**
- Giữ đúng ranh giới sở hữu logic theo domain (khớp `SAD §4.1`)
- Có cơ hội đáp ứng ngân sách latency tốt hơn nhờ cache
**Hệ quả tiêu cực phải sống chung**
- Redis trở thành một điểm phụ thuộc thêm cho luồng Cart & Order — cần thiết kế đường lỗi khi
Redis chậm/hỏng (thuộc `FAIL`, hoạt động 8): fallback về query DB trực tiếp khi cache miss/lỗi
- Rủi ro dữ liệu cache cũ (stale) nếu invalidate không đúng lúc — cần kiểm thử kỹ
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Chuyển sang kiểm tra tại Gateway sau này | Viết lại toàn bộ luồng xác thực + đặt lại ranh giới logic — không khuyến khích |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Đo `QAS-003`/`QAS-004` với thiết kế cache cụ thể | QA + Tech Lead | GĐ3 (trước go-live) | `FIT`/bài đo GĐ3 |
| Thiết kế cơ chế invalidate cache | SA (hoạt động `sec`) + Tech Lead | GĐ2 tiếp theo | `SEC_e-commerce` §2 |
| Thiết kế fallback khi Redis lỗi/chậm | SA (hoạt động `fail`) | GĐ2 tiếp theo | `FAIL_e-commerce` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Test IDOR tự động (gọi API với token/session không phải chủ sở hữu) — 0 lượt thành công | Integration test (tương ứng `AC-US003-12/13` BA) | CI + staging trước mỗi release | — (đã có ở `QAS-010`) |
| API Gateway (`CMP-01`) không chứa logic authz chi tiết theo dữ liệu | Code review + kiến trúc lint | CI | `FIT-10` (ứng viên) |
| Latency `GET`/`PATCH`/`DELETE /v1/cart*` đạt ngân sách sau khi có cache | k6 load test | Staging, trước mỗi release | — (đã có ở `QAS-003`/`004`) |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Bài đo `QAS-003`/`004` sau khi có cache vẫn không đạt ngân sách | Vượt p95 đề xuất | Tech Lead |
| Phát hiện lỗi cache invalidate gây IDOR hoặc dữ liệu cũ ảnh hưởng nghiệp vụ | Bất kỳ sự cố production | Security/SRE |
## 8. Tham chiếu
- POC: Không bắt buộc — khuyến nghị đo trước `Accepted`
- Bài đo: `QAS-003`, `QAS-004`, `QAS-010`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có người Security để ký (`ADL §8` việc #7) — ngoại lệ dự án chạy thử không thay được chữ ký chuyên môn Security |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.
- Tài liệu ngoài: `RBAC_CartCheckout_v1.0.md §2` (BA)
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-007`, `QAS_e-commerce_v1.0.md §A4` (xung đột dòng 1)

View File

@@ -0,0 +1,180 @@
# ADR-008 — Vùng lưu trữ dữ liệu cá nhân (residency) và chính sách chia sẻ PII cho seller khi tách đơn
*Tên file: `adr/ADR-008_vung-luu-tru-pii-chinh-sach-chia-se-seller.md`*
| | |
|---|---|
| **Status** | `Proposed` *(🔴 — phủ quyết thuộc Security/Legal, chưa có người, xem `OQ-007`)* |
| **Date** | 2026-09-15 |
| **Người quyết** | **Security/Legal** (theo `decision-radar.md §5` — "Bảo mật, quyền riêng tư": đề xuất SA, chốt Security, phủ quyết Security). **Chưa có đại diện Pháp chế/Bảo mật được chỉ định** — `OQ-007` mở từ GĐ1, kế thừa `RISK-03` của BA |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~7/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-008` · `QAS-011` · `CON-06` · `ASM-04` · `RBAC_CartCheckout §4` (BA) · `OQ-007` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-008` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Khi tách đơn theo seller (`ADR-003`), hệ thống phải chia sẻ một số trường PII của khách hàng
(địa chỉ giao hàng, số điện thoại người nhận) cho seller tương ứng để họ giao hàng. `DRV-07` ghi
nhận: việc này **chưa được rà soát** theo Nghị định 13/2023 vì dự án chưa có đại diện Pháp
chế/Bảo mật được chỉ định (`OQ-007`, kế thừa `RISK-03` của BA). `RBAC_CartCheckout §4` (BA) đã
đánh dấu "mức che dữ liệu khi hiển thị cho seller — chưa chốt". `ASM-04` (residency
`ap-southeast-1` đủ đáp ứng NĐ13/2023) cũng chưa được xác nhận.
🔴 **Đây là ADR mà SA KHÔNG có thẩm quyền chốt** — theo `decision-radar.md §5` và `D10`, quyết
định bảo mật/quyền riêng tư thuộc Security, phủ quyết thuộc Security. SA chỉ chuẩn bị phương án
và danh sách trường để người có thẩm quyền quyết.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `CON-06` | NĐ13/2023 — bảo vệ dữ liệu cá nhân, cứng, không thương lượng |
| `ASR-008` | Data-minimization khi chia sẻ PII cho seller + xác nhận residency trước go-live |
| `QAS-011` | 100% trường PII rà soát trước go-live, 0 trường dư thừa |
| `RBAC_CartCheckout §4` (BA) | Mức che dữ liệu khi hiển thị cho seller — chưa chốt |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| NĐ13/2023 áp dụng cho cả PII khách hàng và giấy tờ KYC seller | 🟢 Đã kiểm chứng | `CON-06`, pháp luật hiện hành |
| Region `ap-southeast-1` (Singapore) đáp ứng đủ yêu cầu residency của NĐ13/2023 | 🔴 Giả định (`ASM-04`) | Chưa có xác nhận Pháp chế — NĐ13/2023 không yêu cầu dữ liệu phải nằm trong lãnh thổ VN tuyệt đối trong mọi trường hợp, nhưng cần đánh giá cụ thể theo loại dữ liệu |
| Danh sách trường PII "cần thiết để giao hàng" đã đầy đủ và không dư thừa | 🔴 Giả định | SA đề xuất ở §2, chưa có Pháp chế/Bảo mật rà soát |
## 2. Phương án đã cân nhắc
### PA-1 — Chia sẻ toàn bộ hồ sơ khách hàng cho seller (không data-minimization)
| | |
|---|---|
| **Mô tả** | Khi tách đơn, gửi toàn bộ thông tin `Customer` (bao gồm email, lịch sử mua hàng…) cho seller tương ứng |
| **Ưu** | Đơn giản nhất về mặt kỹ thuật — không cần lọc trường |
| **Nhược** | Vi phạm rõ nguyên tắc data-minimization của NĐ13/2023; tăng đáng kể bề mặt rủi ro nếu seller bị lộ dữ liệu (seller không có cùng mức kiểm soát bảo mật như sàn) |
| **Chi phí đảo ngược** | Cao — nếu vi phạm bị phát hiện sau go-live, phải thu hồi truy cập + có thể chịu chế tài pháp lý |
### PA-2 — Whitelist trường tối thiểu cần thiết để giao hàng *(đề xuất, chờ Security/Legal chốt)*
| | |
|---|---|
| **Mô tả** | Chỉ chia sẻ: `recipient_name`, `phone`, `address_line` cho seller tương ứng với `OrderSeller` của họ — không chia sẻ email, lịch sử mua hàng, hay thông tin của các seller khác trong cùng `Order` |
| **Ưu** | Đáp ứng nguyên tắc data-minimization; giới hạn bề mặt rủi ro đúng phạm vi cần thiết cho nghiệp vụ (giao hàng) |
| **Nhược** | Cần cơ chế lọc trường chính xác ở tầng API (không được lộ nhầm trường khi trả response cho seller); cần review định kỳ khi thêm trường mới vào `Customer`/`Order` |
| **Chi phí đảo ngược** | Thấp — mở rộng whitelist sau này nếu cần thêm trường là thay đổi nhỏ, không phải viết lại |
### PA-3 — Không chia sẻ trực tiếp, dùng dịch vụ vận chuyển làm trung gian (ẩn danh hoá địa chỉ)
| | |
|---|---|
| **Mô tả** | Sàn không gửi PII trực tiếp cho seller; địa chỉ giao hàng được gửi thẳng cho GHN/GHTK, seller chỉ nhận mã vận đơn để đóng gói mà không biết địa chỉ cụ thể của khách |
| **Ưu** | Giảm bề mặt rủi ro nhiều nhất — seller không bao giờ thấy PII khách hàng |
| **Nhược** | Không khớp mô hình vận hành thực tế của marketplace VN hiện tại (seller thường tự đóng gói và ghi địa chỉ lên đơn hàng, không phải drop-shipping qua đơn vị vận chuyển trung gian hoàn toàn); cần thay đổi quy trình vận hành seller đáng kể — vượt phạm vi MVP |
| **Chi phí đảo ngược** | Cao — thay đổi mô hình vận hành sau go-live ảnh hưởng trải nghiệm seller đã quen |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Đáp ứng data-minimization (`CON-06`, `QAS-011`) | ❌ | ✅ | ✅ Cao nhất |
| Khớp mô hình vận hành seller hiện tại | ✅ | ✅ | ❌ Cần thay đổi lớn |
| Độ phức tạp kỹ thuật thêm | Thấp | Trung bình (lọc trường) | Cao (tích hợp vận chuyển sâu hơn) |
## 3. Quyết định
> **Đề xuất PA-2 — Whitelist trường tối thiểu (`recipient_name`, `phone`, `address_line`) cho
> seller tương ứng với `OrderSeller` của họ.**
🔴 **Đây là đề xuất của SA, chưa phải quyết định.** Theo `D10`, chấp nhận rủi ro bảo mật/quyền
riêng tư là quyết định của Security/Legal, không phải của SA. ADR này giữ `Proposed` cho tới khi
có đại diện Pháp chế/Bảo mật thật rà soát và ký.
**Vì sao đề xuất PA-2:** Cân bằng giữa đáp ứng data-minimization (loại PA-1) và khả thi vận hành
trong MVP (loại PA-3, vốn cần thay đổi mô hình vận hành seller ngoài phạm vi hiện tại).
**Phạm vi áp dụng (nếu được chốt):** Toàn bộ luồng tách đơn theo seller (`ADR-003`) và mọi API
trả dữ liệu `OrderSeller` cho phía Seller Management.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| PO chỉ định đại diện Pháp chế/Bảo mật | PO | 🔴 **Chưa có người** (`OQ-007`, mở từ GĐ1) |
| Đại diện đó rà soát danh sách trường whitelist ở §2 PA-2 và ký chấp nhận | Security/Legal | Chờ có người |
| Xác nhận `ASM-04` — region `ap-southeast-1` đáp ứng đủ residency NĐ13/2023 | Security/Legal | Chưa xác nhận |
🔴 **ADR này chặn hoàn toàn cho tới khi có người** — không có ai để ký ngoài SA đề xuất. Đây là
điểm chặn nghiêm trọng nhất trong 12 ADR (theo `GUIDE.md`: "phát hiện muộn nhất và đau nhất luôn
đến từ đây").
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Chia sẻ toàn bộ hồ sơ | Vi phạm rõ data-minimization của NĐ13/2023 | `CON-06`, `QAS-011` | Không xét lại — vi phạm pháp luật |
| PA-3 — Ẩn danh hoá qua vận chuyển trung gian | Không khớp mô hình vận hành seller hiện tại; vượt phạm vi MVP | Mô hình vận hành seller đã chốt ở brief | Nếu về sau chuyển sang mô hình fulfillment tập trung (kho sàn quản lý đóng gói) — xét lại, ngoài phạm vi MVP hiện tại |
## 5. Hệ quả
**Hệ quả tích cực** *(nếu PA-2 được Security/Legal chấp nhận)*
- Giới hạn bề mặt rủi ro đúng phạm vi cần thiết cho nghiệp vụ giao hàng
- Có cơ sở whitelist rõ ràng để audit định kỳ
**Hệ quả tiêu cực phải sống chung**
- Cần cơ chế lọc trường chính xác ở tầng API — rủi ro lộ nhầm trường nếu code thay đổi không rà
soát lại whitelist
- Chưa có người ký — dự án phải chấp nhận rủi ro pháp lý mở cho tới khi có đại diện Pháp chế
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Mở rộng chia sẻ thêm trường sau khi đã chốt whitelist hẹp | Cần rà soát lại toàn bộ với Pháp chế trước khi thay đổi — không tự ý mở rộng |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| PO chỉ định đại diện Pháp chế/Bảo mật | PO | Trước go-live, càng sớm càng tốt | `OQ-007` |
| Đại diện đó rà soát whitelist + xác nhận residency | Security/Legal | Sau khi có người | — |
| Thiết kế cơ chế lọc trường ở tầng API | SA (hoạt động `sec`/`dat`) | GĐ2 tiếp theo | `SEC_e-commerce`, `DAT_e-commerce` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Response API trả cho Seller Management chỉ chứa đúng trường whitelist | Contract test / schema validation | CI | `FIT-11` (ứng viên) |
| Audit định kỳ so khớp trường thực tế truyền cho seller vs whitelist đã duyệt | Review thủ công định kỳ bởi Security/Legal | Hàng quý (đề xuất, chưa chốt tần suất) | `⚠️ Khuyến nghị — không tự kiểm được hoàn toàn, cần review thủ công` |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Có đại diện Pháp chế/Bảo mật được chỉ định | Ngay khi có | PO |
| Phát hiện lộ trường PII dư thừa qua audit | Bất kỳ phát hiện nào | Security |
| NĐ13/2023 hoặc quy định liên quan thay đổi | Khi có văn bản pháp luật mới | Legal |
## 8. Tham chiếu
- POC: Không áp dụng
- Bài đo: Audit thủ công định kỳ (chưa chốt tần suất)
- Tài liệu ngoài: `RBAC_CartCheckout_v1.0.md §4` (BA), `RISK_CartCheckout_v1.0.md RISK-03` (BA)
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chặn hoàn toàn — PO chưa chỉ định đại diện Pháp chế/Bảo mật (`OQ-007`); không ai có thẩm quyền ký |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-008`, `ARISK_e-commerce_v1.0.md §3 ARISK-06`

View File

@@ -0,0 +1,169 @@
# ADR-009 — Chiến lược HA/DR cho nhóm dịch vụ giao dịch lõi (Payment, Cart & Order, Identity & Access): RPO ≤15 phút, RTO ≤1 giờ
*Tên file: `adr/ADR-009_chien-luoc-ha-dr-dich-vu-giao-dich-loi.md`*
| | |
|---|---|
| **Status** | `Proposed` *(🔴 radar ≥8 — theo `decision-radar.md §2`/§6, **không được chuyển `Accepted` khi chưa diễn tập DR**; đây cũng là điều kiện tiên quyết ký AG2, không chỉ điều kiện riêng của ADR này)* |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Ops/SRE (hạ tầng/HA-DR — `decision-radar.md §5`: đề xuất SA, chốt SA+SRE, phủ quyết PO nếu vượt ngân sách) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~9/10** — 🔴 bắt buộc diễn tập DR trước `Accepted` |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-010` · `QAS-008` · `OQ-022` · `DEC-12` · `CMP-02`, `CMP-04`, `CMP-11` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-009` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Payment, Cart & Order, Identity & Access (`ASR-010` gọi là "nhóm giao dịch cốt lõi") phải có khả
năng khôi phục sau sự cố với RPO ≤15 phút và RTO ≤1 giờ. `QAS-008` đã ghi rõ: con số này **kế
thừa nguyên trạng** từ `SAD.md §5.3.2/§9.5.2` (tài liệu trước khi có pipeline `sa-lifecycle`),
**chưa được SA thiết kế lại** ở lượt chạy này, và **chưa từng diễn tập** — theo `SKILL.md` mục 7:
"RTO/RPO chưa diễn tập là RTO/RPO trên giấy". `DEC-12` (duyệt từng phần `QAS`) đã chốt điều kiện:
diễn tập DR phải thực hiện **trước khi ký AG2** — đây không chỉ là điều kiện riêng của ADR này mà
là điều kiện tiên quyết của cả gate AG2.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-010` | RPO ≤15 phút, RTO ≤1 giờ cho Payment/Cart&Order/Identity — phải chứng minh bằng diễn tập trước AG2 |
| `QAS-008` | Con số kế thừa từ `SAD.md`, chưa thiết kế lại, chưa diễn tập |
| `OQ-022` | Lịch diễn tập DR đầu tiên — vẫn chờ Ops/SRE, đã chốt là điều kiện bắt buộc AG2 (`DEC-12`) |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| RPO ≤15 phút / RTO ≤1 giờ là mục tiêu đã "chốt" trong tài liệu gốc | 🟡 Ước lượng có cơ sở | `SAD.md §5.3.2/§9.5.2` — nhưng là tài liệu kỹ thuật trước pipeline `sa-*`, chưa qua AG2 |
| Kiến trúc Multi-AZ hiện tại (`SAD §7` deployment view) đủ để đạt RPO/RTO này | 🔴 Giả định | Chưa diễn tập failover thật |
| Đội Ops/SRE sau go-live đủ năng lực thực hiện failover trong 1 giờ | 🔴 Giả định | `OQ-008` (số người/năng lực Ops chưa xác nhận), `CON-08` |
## 2. Phương án đã cân nhắc
### PA-1 — Multi-AZ trong 1 region (`ap-southeast-1`), backup point-in-time, không multi-region
| | |
|---|---|
| **Mô tả** | RDS Multi-AZ (failover tự động trong region), ECS Fargate chạy ≥2 AZ, backup PITR cho RDS (WAL archiving liên tục để đạt RPO ≤15 phút); DR = khôi phục từ backup/failover AZ trong cùng region |
| **Ưu** | Chi phí thấp hơn multi-region đáng kể; đủ chống chịu mất 1 AZ (sự cố phổ biến nhất); khớp `TCO §3.2` hiện tại (không phát sinh chi phí region thứ hai) |
| **Nhược** | Không chống được sự cố toàn bộ region `ap-southeast-1` (hiếm nhưng đã từng xảy ra với các nhà cung cấp cloud); RTO ≤1 giờ cho trường hợp phải khôi phục từ backup (không phải failover AZ) **chưa diễn tập** — có thể lạc quan hơn thực tế |
| **Chi phí đảo ngược** | Trung bình — nâng cấp lên multi-region sau này là dự án hạ tầng riêng, không phải cấu hình nhỏ |
### PA-2 — Multi-region active-passive (thêm region dự phòng, VD `ap-northeast-1`)
| | |
|---|---|
| **Mô tả** | Toàn bộ 3 service lõi có bản sao passive ở region thứ hai, đồng bộ liên tục, failover thủ công/tự động sang region đó khi region chính sập |
| **Ưu** | Chống được cả sự cố toàn region — RTO/RPO ổn định hơn trong kịch bản xấu nhất |
| **Nhược** | Tăng chi phí hạ tầng đáng kể (gấp ~1,5-2 lần cho 3 service lõi) — chưa có trong `TCO §3.2` hiện tại, cần PO duyệt ngân sách bổ sung; residency dữ liệu (NĐ13/2023) cần xác nhận lại nếu region thứ hai không phải `ap-southeast-1` (liên quan `ADR-008`) |
| **Chi phí đảo ngược** | Thấp — có multi-region rồi thu hẹp về 1 region dễ hơn chiều ngược lại |
### Bảng so sánh
| Tiêu chí | PA-1 (Multi-AZ, 1 region) | PA-2 (Multi-region active-passive) |
|---|---|---|
| Chi phí hạ tầng thêm | Thấp (đã có trong `TCO`) | Cao — cần PO duyệt bổ sung |
| Chống chịu mất 1 AZ | ✅ | ✅ |
| Chống chịu mất toàn region | ❌ | ✅ |
| Khớp ngân sách hiện tại (`CON-01`, `TCO §3.2`) | ✅ | ❌ Chưa có trong ngân sách |
| Rủi ro residency nếu region 2 khác `ap-southeast-1` | N/A | Cần xác nhận lại (`ADR-008`) |
## 3. Quyết định
> **Chọn PA-1 — Multi-AZ trong `ap-southeast-1`, backup point-in-time, không multi-region ở
> MVP.**
**Vì sao:** Khớp ngân sách hiện tại (`TCO §3.2` chưa tính chi phí multi-region) và đủ chống chịu
sự cố phổ biến nhất (mất 1 AZ). Multi-region là đánh đổi ngân sách vs độ sẵn sàng — thuộc thẩm
quyền PO (`D10`), chưa có đề nghị PO tăng ngân sách cho việc này.
**Phạm vi áp dụng:** Payment, Cart & Order, Identity & Access (nhóm giao dịch cốt lõi theo
`ASR-010`). Không áp dụng cho Nhóm Hỗ trợ (chưa có yêu cầu RPO/RTO tương đương).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| **Diễn tập DR (failover drill) thật** đạt RPO ≤15 phút, RTO ≤1 giờ | Ops/SRE | 🔴 **Chưa từng diễn tập** (`OQ-022`) |
| Lịch diễn tập đầu tiên được xác nhận | Ops/SRE | Mở |
| Đây là điều kiện tiên quyết ký AG2 (không chỉ điều kiện riêng ADR này) | Tech Lead + Security + Ops/SRE | Theo `DEC-12` |
🔴 **Bắt buộc theo `decision-radar.md §2`/§6** — điểm radar ~9 (≥8) đồng nghĩa ADR này **không
được chuyển `Accepted`** cho tới khi có diễn tập DR thật. Nếu diễn tập cho kết quả không đạt: ghi
nhận vào `ARISK` mới, đánh giá lại kiến trúc backup/failover trước khi thử lại — không hạ thấp
tiêu chí RPO/RTO để "cho qua".
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-2 — Multi-region active-passive | Chi phí thêm chưa có trong ngân sách đã duyệt (`TCO §3.2`); chưa có nhu cầu nghiệp vụ rõ ràng cho chống chịu mất toàn region ở MVP | `CON-01`, `TCO §3.2` | Nếu PO chấp nhận tăng ngân sách **và** đánh giá rủi ro mất toàn region là không chấp nhận được cho MVP — trình bảng đánh đổi cho PO quyết (`D10`), chưa có yêu cầu này |
## 5. Hệ quả
**Hệ quả tích cực**
- Khớp ngân sách hiện tại — không cần PO duyệt thêm chi phí ngoài `TCO §3.2`
- Đủ chống chịu sự cố phổ biến nhất (mất 1 AZ) mà không cần độ phức tạp vận hành của
multi-region
**Hệ quả tiêu cực phải sống chung**
- Không chống được sự cố toàn region `ap-southeast-1` — rủi ro đã ghi nhận, chấp nhận có ý thức
cho tới khi PO quyết định khác
- RTO ≤1 giờ cho khôi phục từ backup (không phải failover AZ tự động) phụ thuộc năng lực đội
Ops/SRE thật — chưa xác nhận (`OQ-008`)
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Nâng cấp lên multi-region sau go-live | Dự án hạ tầng riêng — thiết kế đồng bộ dữ liệu xuyên region, xác nhận lại residency, tăng chi phí vận hành liên tục |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Xác nhận lịch diễn tập DR đầu tiên | Ops/SRE | Trước khi ký AG2 | `OQ-022` |
| Chạy diễn tập DR, ghi kết quả | Ops/SRE | Trước khi `Accepted` và trước AG2 | `ARISK_e-commerce_v1.0.md` (kết quả mới) |
| Trình PO bảng đánh đổi multi-region nếu Ops/SRE thấy cần | SA + Ops/SRE | Sau lần diễn tập đầu tiên nếu phát hiện rủi ro cao | `TCO` (cập nhật nếu PO chấp nhận) |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Diễn tập failover AZ (dừng 1 AZ giả lập) đo RTO thật | Chaos engineering thủ công / AWS Fault Injection Simulator | Staging, sau đó production (giờ thấp điểm) | — (kết quả ghi vào `ARISK`) |
| Backup PITR chạy đúng tần suất để đạt RPO ≤15 phút | Kiểm tra cấu hình WAL archiving/backup schedule | CI/CD (kiểm tra hạ tầng) | `FIT-12` (ứng viên) |
| Runbook failover được viết và diễn tập theo đúng bước | Review thủ công + diễn tập | Định kỳ (đề xuất 2 lần/năm, chưa chốt) | `⚠️ Khuyến nghị — không tự kiểm được, cần diễn tập thủ công` |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Diễn tập DR không đạt RPO/RTO | RPO > 15 phút hoặc RTO > 1 giờ | Ops/SRE |
| PO yêu cầu nâng mức sẵn sàng lên multi-region | Khi PO chấp nhận chi phí bổ sung | PO |
| Sự cố production thật cho thấy RTO/RPO thực tế khác đo ở diễn tập | Bất kỳ lần nào | SRE |
## 8. Tham chiếu
- POC: Diễn tập DR (chưa chạy) — xem `ARISK_e-commerce_v1.0.md` (kết quả sẽ bổ sung)
- Bài đo: `FIT-12` (ứng viên GĐ3)
- Tài liệu ngoài: `QAS_e-commerce_v1.0.md §A3 QAS-008`, `e-commerce/docs/sections/09-van-hanh-kiem-thu.md §9.5.2` (kế thừa nguyên trạng)
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-010`, `DEC-12` (00-index/DEC_e-commerce.md)
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Radar 9 — bắt buộc diễn tập DR (failover drill) PASS trước khi `Accepted`, cũng là điều kiện tiên quyết `AG2`; chưa chạy |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,166 @@
# ADR-010 — Kiến trúc observability & alerting cho dịch vụ giao dịch lõi
*Tên file: `adr/ADR-010_kien-truc-observability-alerting.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Ops/SRE (hạ tầng — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~6/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-011` · `QAS-013` · `CMP-02`, `CMP-04`, `CMP-11` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-010` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Sự cố lỗi 5xx tăng đột biến ở Cart & Order, Payment, Identity & Access phải được cảnh báo tự
động trong ≤1 phút; on-call phải acknowledge trong ≤15 phút (`ASR-011`, `QAS-013`). Cần chuẩn
log/metric/trace thống nhất cho 3 service lõi và pipeline cảnh báo kết nối kênh on-call.
`QAS-013` ghi rõ ngưỡng cảnh báo **kế thừa nguyên trạng** từ `SAD.md §9.4.1`, chưa từng diễn tập
inject lỗi.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-011` | Chuẩn log/metric/trace thống nhất, pipeline cảnh báo ≤1 phút, on-call ack ≤15 phút |
| `QAS-013` | Diễn tập inject lỗi đo thời gian từ inject tới cảnh báo — chưa từng diễn tập |
| `CON-08` | Đội Ops trực theo ca + escalation 24/7 — giả định, chưa xác nhận số người thật (`OQ-008`) |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Cần cảnh báo tự động cho 3 service lõi trong ≤1 phút | 🟢 Đã kiểm chứng | `ASR-011`, yêu cầu doanh thu phụ thuộc trực tiếp |
| CloudWatch Alarms/Synthetics đủ đáp ứng ngưỡng ≤1 phút trong ngữ cảnh này | 🔴 Giả định | Chưa diễn tập inject lỗi |
| Đội Ops đủ người để acknowledge ≤15 phút 24/7 | 🔴 Giả định | `OQ-008` chưa xác nhận |
## 2. Phương án đã cân nhắc
### PA-1 — CloudWatch Logs/Metrics/Alarms native (AWS) + PagerDuty/OpsGenie cho on-call
| | |
|---|---|
| **Mô tả** | Dùng CloudWatch Logs cho log tập trung, CloudWatch Metrics/Alarms cho ngưỡng cảnh báo, X-Ray cho trace, kết nối PagerDuty/OpsGenie cho on-call rotation |
| **Ưu** | Managed hoàn toàn, tích hợp sẵn với ECS Fargate/RDS/SQS — không cần vận hành thêm cluster; chi phí theo mức dùng, dễ ước lượng |
| **Nhược** | CloudWatch Logs Insights có giới hạn về khả năng truy vấn phức tạp so với ELK/Datadog; chi phí có thể tăng nhanh nếu log volume lớn không được kiểm soát retention |
| **Chi phí đảo ngược** | Trung bình — chuyển sang stack khác (Datadog, ELK) sau này cần viết lại instrumentation code |
### PA-2 — Self-hosted ELK/Grafana stack
| | |
|---|---|
| **Mô tả** | Tự vận hành Elasticsearch/Logstash/Kibana + Prometheus/Grafana trên ECS/EKS riêng |
| **Ưu** | Khả năng truy vấn/dashboard linh hoạt hơn, không phụ thuộc vendor lock-in AWS |
| **Nhược** | Thêm cụm phải vận hành/patch/scale — vi phạm Conway với năng lực team hiện tại (cùng lý lẽ với việc loại Kafka/MSK ở `ADR-005`); tăng chi phí vận hành đáng kể |
| **Chi phí đảo ngược** | Cao — đã đầu tư vận hành cụm riêng thì chuyển sang managed service sau là bỏ phí đầu tư ban đầu |
### PA-3 — Datadog/New Relic (SaaS bên thứ ba)
| | |
|---|---|
| **Mô tả** | Dùng nền tảng observability SaaS thương mại, tích hợp qua agent/SDK |
| **Ưu** | Tính năng phong phú (APM, log correlation, dashboard sẵn), không cần vận hành hạ tầng riêng |
| **Nhược** | Chi phí SaaS theo host/dữ liệu có thể cao hơn CloudWatch ở quy mô lớn (`DRV-04` hàng nghìn–chục nghìn concurrent); thêm một nhà cung cấp bên ngoài cần đánh giá hợp đồng/dữ liệu residency (log có thể chứa thông tin nhạy cảm) |
| **Chi phí đảo ngược** | Trung bình — có thể chuyển sang CloudWatch/khác sau nhưng mất dashboard đã cấu hình |
### Bảng so sánh
| Tiêu chí | PA-1 (CloudWatch) | PA-2 (Self-hosted ELK) | PA-3 (Datadog/New Relic) |
|---|---|---|---|
| Khớp năng lực team (`CON-05`) | ✅ | ❌ Thêm cụm phải vận hành | ✅ |
| Chi phí dự đoán được | ✅ | 🔶 Chi phí nhân sự vận hành ẩn | 🔶 Có thể tăng theo scale |
| Tích hợp sẵn với AWS managed service đã chọn | ✅ | 🔶 Cần cấu hình thêm | 🔶 Cần agent riêng |
| Đã có trong `TCO §3.2`/ngân sách hiện tại | ✅ | ❌ Chưa tính | ❌ Chưa tính |
## 3. Quyết định
> **Chọn PA-1 — CloudWatch Logs/Metrics/Alarms + X-Ray (trace) + PagerDuty/OpsGenie (on-call).**
**Vì sao:** Khớp năng lực team hiện tại (không thêm cụm phải vận hành, nhất quán với hướng managed
service đã chọn ở `ADR-001`/`ADR-005`) và đã có trong ước lượng ngân sách hiện tại.
**Phạm vi áp dụng:** Toàn bộ 3 service lõi (Payment, Cart & Order, Identity & Access) ở giai đoạn
đầu; mở rộng cho Nhóm Hỗ trợ khi cần.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Diễn tập inject lỗi đo thời gian từ inject tới cảnh báo ≤1 phút | Ops/SRE | Chưa từng diễn tập (`QAS-013`) |
| Xác nhận công cụ on-call cụ thể (PagerDuty hay OpsGenie) và ngân sách | PO/Ops | Chưa chốt |
| Không bắt buộc POC riêng (radar 6, dưới ngưỡng 8) nhưng khuyến nghị diễn tập trước `Accepted` | Ops/SRE | — |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-2 — Self-hosted ELK/Grafana | Thêm cụm phải vận hành, vi phạm Conway với năng lực team | `CON-05` | Nếu về sau team có SRE chuyên trách đủ lớn và cần khả năng truy vấn phức tạp hơn CloudWatch cung cấp — xét lại |
| PA-3 — Datadog/New Relic | Chi phí SaaS chưa có trong ngân sách hiện tại; thêm nhà cung cấp bên ngoài cần đánh giá residency dữ liệu log | `TCO §3.2`, `CON-06` | Nếu CloudWatch không đáp ứng đủ nhu cầu (VD thiếu APM sâu) và PO chấp nhận chi phí thêm — trình bảng đánh đổi |
## 5. Hệ quả
**Hệ quả tích cực**
- Không thêm thành phần hạ tầng phải vận hành riêng — khớp năng lực team
- Chi phí dự đoán được, đã nằm trong ước lượng `TCO`
**Hệ quả tiêu cực phải sống chung**
- Khả năng truy vấn log phức tạp hạn chế hơn ELK/Datadog — có thể cần thêm công cụ bổ sung khi
hệ thống lớn hơn
- Chưa diễn tập inject lỗi — ngưỡng ≤1 phút vẫn là con số trên giấy cho tới khi diễn tập
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Chuyển sang Datadog/ELK sau khi đã cấu hình CloudWatch dashboard | Viết lại instrumentation + dashboard, di chuyển alert đã cấu hình |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Diễn tập inject lỗi đo thời gian cảnh báo | Ops/SRE | Trước go-live | `ARISK`/`QAS-013` (kết quả mới) |
| Chốt công cụ on-call cụ thể (PagerDuty/OpsGenie) | PO/Ops | GĐ2 tiếp theo (`inf`) | `INF_e-commerce` |
| Chuẩn hoá định dạng log + correlation id cho 3 service lõi | Tech Lead | GĐ3 (`AGD`) | `AGD` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Diễn tập inject lỗi (dừng task ECS) đo thời gian tới cảnh báo | Chaos thủ công | Staging trước go-live, định kỳ sau đó | — (kết quả ghi vào `ARISK`) |
| Mọi service lõi ghi log theo định dạng chuẩn có `correlation_id` | Lint log format trong CI | CI | `FIT-13` (ứng viên) |
| Alert có kết nối kênh on-call thật, test bằng cảnh báo giả lập | Kiểm tra cấu hình PagerDuty/OpsGenie | Định kỳ (đề xuất hàng tháng) | `⚠️ Khuyến nghị — cần kiểm thủ công định kỳ` |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Diễn tập inject lỗi cho thấy thời gian cảnh báo > 1 phút | Bất kỳ lần nào | Ops/SRE |
| Chi phí CloudWatch vượt ngân sách dự kiến đáng kể | > 20% so với `TCO` | SRE/tài chính |
## 8. Tham chiếu
- POC: Diễn tập inject lỗi (chưa chạy)
- Bài đo: `FIT-13` (ứng viên GĐ3)
- Tài liệu ngoài: `QAS_e-commerce_v1.0.md §A3 QAS-013`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-011`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có chữ ký thật Ops/SRE; khuyến nghị diễn tập alerting trước khi chốt |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,159 @@
# ADR-011 — Chính sách resilience chung khi đối tác thanh toán/vận chuyển bên ngoài lỗi (timeout/retry/fallback)
*Tên file: `adr/ADR-011_chinh-sach-resilience-doi-tac-ngoai.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (tích hợp — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~6/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-013` · `CON-04` · `ARISK-03` · `ARISK-04` · `CMP-10`, `CMP-11` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-011` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Hệ thống phải tích hợp đúng 4 đối tác đã chốt: VNPay, Momo (thanh toán), GHN, GHTK (vận chuyển,
có fallback chéo) — `CON-04`. Không đối tác nào có sandbox/hợp đồng thật tại thời điểm này
(`CTX §4.2`). `ASR-013` yêu cầu một chính sách resilience **chung** cho cả 4 đối tác (timeout,
retry, idempotent) thay vì mỗi đội tự chọn một kiểu khác nhau — đây là quyết định "thiết lập tiền
lệ" theo `decision-radar.md §4` (ngoại lệ để nhỏ thành ADR): áp dụng cho mọi tích hợp đối tác
ngoài tương lai, không chỉ 4 đối tác hiện tại.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-013` | Chính sách timeout/retry/idempotent nhất quán cho VNPay/Momo/GHN/GHTK; GHN/GHTK cần fallback chéo |
| `CON-04` | 4 đối tác đã chốt danh tính, chưa có sandbox/hợp đồng thật |
| `ARISK-03` | VNPay/Momo — webhook chưa xác minh khả thi gần thời gian thực |
| `ARISK-04` | GHN/GHTK — cơ chế fallback chéo chưa kiểm chứng hoạt động đúng giữa 2 API khác nhau |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Danh tính 4 đối tác đã chốt (không phải chờ chọn) | 🟢 Đã kiểm chứng | `CON-04` |
| Timeout đề xuất VNPay/Momo 10s, GHN/GHTK 8s | 🔴 Giả định — kế thừa từ `SAD.md §3.4`, chưa kiểm chứng sandbox thật | `CTX §4.2` |
| Fallback GHN↔GHTK hoạt động đúng khi chuyển đổi API giữa chừng | 🔴 Giả định | `ARISK-04`, chưa có tài liệu API/sandbox thật |
## 2. Phương án đã cân nhắc
### PA-1 — Mỗi đội tự thiết kế timeout/retry riêng cho từng đối tác (không có chính sách chung)
| | |
|---|---|
| **Mô tả** | Không có ADR/chuẩn chung — mỗi dev tích hợp VNPay/Momo/GHN/GHTK tự quyết định timeout/retry theo kinh nghiệm cá nhân |
| **Ưu** | Linh hoạt tối đa cho từng đối tác cụ thể |
| **Nhược** | Không nhất quán — dev khác nhau chọn số khác nhau, khó audit/thay đổi hàng loạt khi cần; đúng bẫy đã cảnh báo ở `GUIDE.md` ("Bỏ qua đường lỗi vì sẽ xử lý lúc code") |
| **Chi phí đảo ngược** | Cao — chuẩn hoá lại sau khi đã có nhiều cách làm khác nhau tốn công rà soát từng điểm tích hợp |
### PA-2 — Chính sách chung: timeout theo loại đối tác + retry có backoff + idempotency key bắt buộc, adapter riêng mỗi đối tác *(chọn)*
| | |
|---|---|
| **Mô tả** | Một chuẩn chung áp dụng cho mọi đối tác ngoài: timeout cụ thể theo loại (thanh toán 10s, vận chuyển 8s — đề xuất, chờ kiểm chứng sandbox), retry ≤3 lần với exponential backoff + jitter, idempotency key bắt buộc cho mọi lời gọi ghi; mỗi đối tác có một adapter/anti-corruption layer riêng để cô lập thay đổi API của họ |
| **Ưu** | Nhất quán, dễ audit và thay đổi hàng loạt; adapter riêng giúp thay đổi API một đối tác không ảnh hưởng đối tác khác; đặt tiền lệ cho đối tác tương lai (khớp `decision-radar.md §4`) |
| **Nhược** | Cần thời gian thiết kế chuẩn chung trước khi bắt tay code từng đối tác; timeout cụ thể vẫn là đề xuất chưa kiểm chứng sandbox thật |
| **Chi phí đảo ngược** | Thấp — điều chỉnh số cụ thể (timeout, số lần retry) sau khi có sandbox thật là thay đổi cấu hình, không phải viết lại kiến trúc |
### Bảng so sánh
| Tiêu chí | PA-1 (Tự do từng đội) | PA-2 (Chuẩn chung + adapter) |
|---|---|---|
| Nhất quán, dễ audit | ❌ | ✅ |
| Đặt tiền lệ cho đối tác tương lai | ❌ | ✅ |
| Cô lập thay đổi API một đối tác | 🔶 Tuỳ đội | ✅ (adapter riêng) |
| Effort thiết kế trước khi code | Thấp | Trung bình |
## 3. Quyết định
> **Chọn PA-2 — Chính sách resilience chung (timeout theo loại đối tác, retry backoff+jitter,
> idempotency key bắt buộc) + adapter riêng cho mỗi đối tác.**
**Vì sao:** Đây là quyết định thiết lập tiền lệ cho mọi tích hợp đối tác ngoài hiện tại và tương
lai (`decision-radar.md §4`) — không nhất quán ngay từ đầu sẽ tốn công chuẩn hoá lại sau, đúng
bẫy đã cảnh báo ở `GUIDE.md`.
**Phạm vi áp dụng:** VNPay, Momo, GHN, GHTK, Email/SMS Provider, và mọi đối tác ngoài tương lai.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Có sandbox thật của cả 4 đối tác để kiểm chứng timeout cụ thể | Tech Lead | Chưa có (`CON-04`, `ARISK-03`/`04`) |
| Kiểm chứng fallback GHN↔GHTK hoạt động đúng | Tech Lead | Chưa kiểm chứng |
| Không bắt buộc POC theo `decision-radar.md §6` (không phải con số hiệu năng lớn) nhưng cần sandbox thật | — | Chờ sandbox |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Mỗi đội tự thiết kế riêng | Không nhất quán, khó audit/thay đổi hàng loạt; vi phạm nguyên tắc "đường lỗi không thể xử lý lúc code" | `GUIDE.md` "Bẫy thường gặp" | Không xét lại — đây là anti-pattern đã có cảnh báo rõ trong bộ SA |
## 5. Hệ quả
**Hệ quả tích cực**
- Nhất quán, dễ audit và thay đổi hàng loạt khi cần đổi chính sách retry/timeout
- Adapter riêng cô lập thay đổi API một đối tác, không lan sang đối tác khác
**Hệ quả tiêu cực phải sống chung**
- Cần thời gian thiết kế chuẩn chung trước khi từng đội bắt tay code — có thể chậm hơn PA-1 ở
giai đoạn đầu
- Timeout cụ thể (10s/8s) vẫn là đề xuất chưa kiểm chứng, có thể cần điều chỉnh khi có sandbox
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Bỏ chuẩn chung, để từng đối tác tự do sau khi đã có nhiều adapter theo chuẩn | Không tốn nhiều nếu chỉ đổi số cụ thể; tốn nhiều nếu đổi cấu trúc adapter |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Đàm phán sandbox 4 đối tác | Tech Lead + PM | Trước GĐ2 `ICD` hoàn tất | `ARISK-03`, `ARISK-04` |
| Thiết kế chi tiết từng phụ thuộc (4 câu hỏi D6) | SA (hoạt động `fail`) | GĐ2 tiếp theo | `FAIL_e-commerce` |
| Viết integration test cho fallback GHN↔GHTK | Tech Lead | GĐ2/GĐ3 | `FIT` (ứng viên) |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Mọi lời gọi ghi tới đối tác ngoài có idempotency key | Code review/lint + integration test | CI | `FIT-14` (ứng viên) |
| Retry có backoff+jitter, không retry storm khi đối tác hồi phục | Integration test giả lập đối tác chậm/hỏng | CI + staging | `FIT-15` (ứng viên) |
| Fallback GHN→GHTK kích hoạt đúng khi GHN lỗi | Integration test riêng cho luồng fallback | Staging, cần sandbox thật | — |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Sandbox thật cho thấy timeout đề xuất sai lệch nhiều | Timeout thật khác đề xuất > 50% | Tech Lead |
| Fallback GHN↔GHTK thất bại trong kiểm thử/production | Bất kỳ lần nào | Tech Lead/SRE |
## 8. Tham chiếu
- POC: Không bắt buộc — cần sandbox thật để kiểm chứng đầy đủ
- Bài đo: `FIT-14`, `FIT-15` (ứng viên GĐ3)
- Tài liệu ngoài: `CTX_e-commerce_v1.0.md §4.2` (trạng thái đối tác), `ARISK_e-commerce_v1.0.md §3 ARISK-03/04`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-013`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chưa có chữ ký thật Tech Lead; cần chạy thử trên sandbox đối tác ngoài trước khi chốt |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,177 @@
# ADR-012 — Ranh giới nhóm triển khai (deployment boundary): Nhóm Giao dịch / Nhóm Hỗ trợ / Payment tách biệt
*Tên file: `adr/ADR-012_ranh-gioi-nhom-trien-khai-theo-domain.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (cấu trúc/ranh giới service — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~7/10** |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-014` · `CON-05` · `OQ-010` · `OQ-026` · `ASM-19` · `ASM-22` · `ASM-23` · `ADR-001` · `SAD §4.1, §7` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-012` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
`ADR-001` đã chọn modular monolith (P2) với 2–3 nhóm triển khai ECS Fargate thay vì ~10 service
độc lập. ADR này là **quyết định cụ thể tiếp theo**: module nào nằm nhóm nào — khác `ADR-001` ở
chỗ đó là quyết định "monolith hay không", còn đây là "ranh giới cụ thể bên trong monolith".
`ASR-014` ghi nhận việc gom module vào nhóm phải phản ánh đúng năng lực đội thi công **thật**,
không chỉ theo giả định "team MVP chuẩn" (`DEC-02`). Hai câu hỏi mở trực tiếp ảnh hưởng ADR này:
`OQ-010` (số đội thi công thật) và `OQ-026` (SAD phát hiện lệch mức chi tiết giữa "2 nhóm ECS
service riêng" và cách `TCO §3.2` tính gộp compute thành 1 dòng).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-014` | Ranh giới nhóm triển khai phải khớp năng lực đội thi công thật |
| `CON-05` | Team giả định "team MVP chuẩn" (`DEC-02`), chưa xác nhận thật |
| `OQ-010` | Tech Lead chưa xác nhận đội thi công thật |
| `OQ-026` | `TCO §3.2` tính gộp compute — chưa rõ là 1 hay 2 ECS service thật |
| `ASM-22` | Identity & Access xếp vào Nhóm Giao dịch — suy luận từ `ASR-010`/`011`, chưa Tech Lead xác nhận |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Payment phải tách biệt hoàn toàn khỏi 2 nhóm còn lại | 🟢 Đã kiểm chứng | `ADR-002` (độc lập với ADR này) |
| Nhóm Giao dịch (Catalog & Inventory, Cart & Order) cần scale nhanh hơn Nhóm Hỗ trợ | 🟡 Ước lượng có cơ sở | `DRV-04`, `OPT §3` — chưa có số đo thật sau go-live |
| Identity & Access thuộc Nhóm Giao dịch (không phải nhóm riêng/Nhóm Hỗ trợ) | 🔴 Giả định (`ASM-22`) | Suy từ `ASR-010`/`011` ("nhóm giao dịch cốt lõi" bao gồm Identity cho HA/DR) — `OPT §3` không nói rõ |
| `TCO §3.2` phản ánh đúng 2 ECS service riêng (không phải 1 service gộp) | 🔴 Giả định (`ASM-23`) | `OQ-026` — chưa Tech Lead/Ops xác nhận |
## 2. Phương án đã cân nhắc
### PA-1 — 1 nhóm ECS duy nhất cho toàn bộ monolith (không tách Nhóm Giao dịch/Nhóm Hỗ trợ)
| | |
|---|---|
| **Mô tả** | Toàn bộ 9 module (trừ Payment) chạy trong 1 ECS Fargate service duy nhất, scale chung |
| **Ưu** | Đơn giản nhất — chỉ 2 đơn vị triển khai (1 monolith + 1 Payment); ít cấu hình network/scaling riêng |
| **Nhược** | Không thể scale-out riêng Catalog/Cart&Order khi flash sale mà không kéo theo cả Nhóm Hỗ trợ — lãng phí tài nguyên hoặc thiếu tài nguyên đúng lúc cần; vi phạm `DRV-04` một phần (mục tiêu chịu tải đỉnh của riêng luồng giao dịch) |
| **Chi phí đảo ngược** | Trung bình — tách thành 2 nhóm sau này cần cấu hình lại network/scaling policy, nhưng không cần migrate dữ liệu (cùng schema-per-module) |
### PA-2 — 2 nhóm ECS: Nhóm Giao dịch (Identity, Catalog, Cart&Order) / Nhóm Hỗ trợ (còn lại) *(chọn tạm)*
| | |
|---|---|
| **Mô tả** | Theo `OPT §3` P2 — Nhóm Giao dịch gồm Identity & Access, Catalog & Inventory, Cart & Order; Nhóm Hỗ trợ gồm Seller Management, Commission & Payout, Promotion & Loyalty, Review, Notification, Shipping & Fulfillment; mỗi nhóm là 1 ECS Fargate service riêng, scale độc lập |
| **Ưu** | Cho phép scale riêng Nhóm Giao dịch khi flash sale mà không ảnh hưởng Nhóm Hỗ trợ (đáp ứng `DRV-04`); khớp đề xuất đã duyệt từng phần ở `OPT §3`/`DEC-06` |
| **Nhược** | Vị trí Identity & Access trong Nhóm Giao dịch là **suy luận của SA** (`ASM-22`), chưa được Tech Lead xác nhận trực tiếp; `OQ-026` cho thấy `TCO` chưa rõ có tính đúng 2 service riêng hay không — nếu `TCO` thực ra tính 1 service, phương án này phát sinh chi phí chưa được duyệt |
| **Chi phí đảo ngược** | Trung bình — đổi thành viên giữa 2 nhóm (VD chuyển Identity sang Nhóm Hỗ trợ) là thay đổi cấu hình + CI/CD, không phải viết lại code (nhờ ranh giới module đã có ở `ADR-001`) |
### PA-3 — 3 nhóm ECS: tách riêng Identity & Access thành nhóm thứ ba (ngoài Nhóm Giao dịch/Nhóm Hỗ trợ)
| | |
|---|---|
| **Mô tả** | Identity & Access là một ECS service riêng biệt, không thuộc Nhóm Giao dịch hay Nhóm Hỗ trợ, vì đây là dịch vụ nền tảng được cả hai nhóm gọi tới |
| **Ưu** | Tách bạch rõ ràng vai trò "hạ tầng xác thực dùng chung" khỏi logic nghiệp vụ Catalog/Cart&Order; có thể scale/HA riêng cho Identity mà không phụ thuộc tải của Catalog |
| **Nhược** | Thêm một đơn vị triển khai thứ 4 (ngoài Payment) — tăng chi phí vận hành so với PA-2; chưa có trong ước lượng `TCO §3.2` nào (chỉ có "monolith" + Payment) |
| **Chi phí đảo ngược** | Thấp — tách Identity ra là thu hẹp phạm vi 1 service, ít rủi ro hơn gộp lại |
### Bảng so sánh
| Tiêu chí | PA-1 (1 nhóm) | PA-2 (2 nhóm, Identity ở Nhóm Giao dịch) | PA-3 (3 nhóm, Identity riêng) |
|---|---|---|---|
| Scale riêng Nhóm Giao dịch khi flash sale (`DRV-04`) | ❌ | ✅ | ✅ |
| Khớp `OPT §3`/`DEC-06` đã duyệt từng phần | ❌ | ✅ | 🔶 Gần khớp, thêm 1 nhóm |
| Khớp `TCO §3.2` hiện tại (chưa rõ, `OQ-026`) | ✅ (nếu TCO tính 1 service) | 🔶 Chờ xác nhận | ❌ Chưa có trong TCO |
| Số đơn vị triển khai | 2 | 3 | 4 |
## 3. Quyết định
> **Đề xuất PA-2 — 2 nhóm ECS: Nhóm Giao dịch (Identity & Access, Catalog & Inventory, Cart &
> Order) / Nhóm Hỗ trợ (5 module còn lại), cộng Payment tách biệt.**
🔴 **Đây là quyết định tạm, chưa đủ điều kiện `Accepted`** — theo `ASM-22`/`ASM-23` và `OQ-010`/
`OQ-026` vẫn mở. Giữ PA-2 làm hướng đi (nhất quán với `OPT §3`/`DEC-06`/`SAD §4.1`) cho tới khi
Tech Lead/Ops xác nhận.
**Vì sao đề xuất PA-2:** Cân bằng giữa khả năng scale riêng Nhóm Giao dịch (`DRV-04`, loại PA-1)
và không phát sinh thêm chi phí vận hành chưa có trong `TCO` (loại PA-3, chờ xác nhận thêm ngân
sách nếu cần).
**Phạm vi áp dụng:** Toàn bộ 9 module ngoài Payment.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| `OQ-010` — Tech Lead xác nhận số đội thật khớp (hoặc đủ để vận hành) 2–3 nhóm triển khai | Tech Lead | Mở |
| `OQ-026` — Tech Lead/Ops xác nhận `TCO §3.2` tính đúng 2 ECS service riêng (không phải 1 gộp) | Tech Lead + Ops/SRE | Mở |
| `OQ-027` — Tech Lead xác nhận vị trí Identity & Access (Nhóm Giao dịch hay riêng/Nhóm Hỗ trợ) | Tech Lead | Mở |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — 1 nhóm ECS duy nhất | Không đáp ứng nhu cầu scale riêng Nhóm Giao dịch khi flash sale | `DRV-04` | Nếu `OQ-026` xác nhận `TCO` thực ra chỉ tính 1 service (không đủ ngân sách cho 2 nhóm riêng) — quay lại PA-1 và ghi nhận giảm điểm tiêu chí "Đáp ứng DRV Must" đã chấm ở `OPT §4` |
| PA-3 — 3 nhóm, Identity riêng | Chưa có trong `TCO §3.2`, tăng chi phí vận hành chưa được PO duyệt | `TCO §3.2`, `CON-01` | Nếu Tech Lead xác nhận Identity cần HA/scale độc lập khỏi Catalog/Cart&Order (VD do tải xác thực rất khác biệt) **và** PO duyệt thêm chi phí — trình bảng đánh đổi |
## 5. Hệ quả
**Hệ quả tích cực**
- Cho phép scale riêng Nhóm Giao dịch khi flash sale mà không lãng phí tài nguyên cho Nhóm Hỗ trợ
- Nhất quán với hướng đi đã duyệt từng phần ở `OPT §3`/`DEC-06`
**Hệ quả tiêu cực phải sống chung**
- Vị trí Identity & Access vẫn là giả định (`ASM-22`) — rủi ro phải đổi cấu hình khi Tech Lead xác
nhận khác
- Nếu `OQ-026` xác nhận `TCO` chưa tính đủ chi phí 2 service riêng, cần cập nhật `TCO` trước khi
ADR này có thể `Accepted`
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi thành viên giữa Nhóm Giao dịch/Nhóm Hỗ trợ sau khi team đã quen vận hành theo ranh giới cũ | Tái tổ chức CI/CD + on-call — ước 1-3 tuần tuỳ mức độ đổi |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Tech Lead xác nhận `OQ-010` (số đội thật) | Tech Lead | Trước khi `Accepted` | `00-index/OQ_e-commerce.md` |
| Tech Lead/Ops xác nhận `OQ-026` (khớp TCO) | Tech Lead + Ops/SRE | Trước khi `Accepted`, trước hoạt động `inf` chốt | `00-index/OQ_e-commerce.md` |
| Tech Lead xác nhận `OQ-027` (vị trí Identity) | Tech Lead | Trước khi `Accepted` | `00-index/OQ_e-commerce.md` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Đúng 2 ECS Fargate service (Nhóm Giao dịch, Nhóm Hỗ trợ) + 1 Payment tồn tại trong hạ tầng | Kiểm tra hạ tầng (Terraform plan review) | CI/CD | `FIT-16` (ứng viên) |
| Module thuộc đúng nhóm theo bảng `CMP-nn` cột "Nhóm triển khai" (`SAD §4.1`) | Review kiến trúc định kỳ | GĐ3 (`DREV`) | — |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| `OQ-010` trả lời team thật khác biệt đáng kể so với giả định | Số BE thật < 50% hoặc > gấp đôi giả định `DEC-02` | Tech Lead |
| `OQ-026` xác nhận `TCO` chỉ tính 1 service (không đủ ngân sách 2 nhóm) | Khi có xác nhận | PO/Tech Lead |
| Tải thật cho thấy Identity cần scale độc lập khỏi Catalog/Cart&Order | Dữ liệu production sau go-live | SRE |
## 8. Tham chiếu
- POC: Không áp dụng
- Bài đo: `FIT-16` (ứng viên GĐ3)
- Tài liệu ngoài: `OPT_e-commerce_v1.0.md §3` (P2), `SAD_e-commerce_v1.0.md §4.1, §7, §10 (ASM-22/23), §11 (OQ-026/027)`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-014`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Chờ Tech Lead + Ops/SRE xác nhận `OQ-010`/`OQ-026`/`OQ-027` |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.

View File

@@ -0,0 +1,351 @@
# ADR-013 — Bất đồng bộ hoá bước khởi tạo thanh toán trong checkout
*Tên file: `adr/ADR-013_bat-dong-bo-hoa-khoi-tao-thanh-toan.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | Tech Lead + PO *(chưa ký thật — xem điều kiện `Accepted` ở §3/§7)* |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **7/10** *(chấm theo `decision-radar.md §2`: Chi phí đảo ngược=2 (>4 tuần — đã lộ ra FE, đổi lại phải thiết kế lại màn hình chờ/polling và state machine) · Bán kính ảnh hưởng=2 (Cart&Order, Payment Service, FE, BA/SRS US-004, ICD) · Chạm `QAS` Must=2 (trực tiếp `QAS-002`) · Ràng buộc dài hạn=1 (ràng trong phạm vi thiết kế hiện tại, xét lại được khi có sandbox thật) · Tranh cãi=0 (lần đầu đề xuất, chưa tranh luận nhiều vòng) → **5–7 ⇒ ADR bắt buộc**, dưới ngưỡng 8 nên không bắt buộc POC trước khi `Accepted`, nhưng vẫn cần bài đo xác nhận theo §6)* |
| **Supersedes** | — *(không supersede ADR nào — luồng đồng bộ trước đó là một phần `SAD §6.1` "duyệt từng phần" dưới ngoại lệ gate, chưa từng là một `ADR` riêng, nên không có ADR nguồn để supersede; `SAD §6.1` được sửa trực tiếp kèm dẫn chiếu `ADR-013`, xem `SAD_e-commerce_v1.0.md` v1.3 Change Log)* |
| **Superseded by** | — |
| **Liên quan** | `ASR-004` · `QAS-002` · `DRV-02` · `CON-04` · `ARISK-03` · `ADR-004` (idempotency/đối soát webhook) · `ADR-005` (SQS FIFO/EventBridge) · `ADR-011` (chính sách resilience đối tác ngoài) · `OQ-034` (đã chọn hướng B, `DEC-22`) |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-013` và đổi trạng thái bản cũ thành `Superseded by`. Không sửa nội dung ADR
> đã `Accepted`, không xoá ADR đã `Rejected`.
---
## 0. Ghi chú Preflight & ngoại lệ gate — bắt buộc đọc trước
AG1 vẫn chưa ký thật; AG2 (gate của chính GĐ2) chưa tới hạn. Theo `SKILL.md`, hoạt động 1 (`QAS`)
→ 2 (`ASR`) → 3 (`SAD`) đã hoàn tất và được duyệt từng phần từ trước (`DEC-12/14/16`); hoạt động 4
(`ICD`) và 8 (`FAIL`) đã chạy (`DEC-18…22`). Điều kiện đầu vào cho việc viết `ADR-013` (kết luận
của `FAIL §1.2`, đã được điều phối dự án chọn Lựa chọn B qua `OQ-034`/`DEC-22`) đã đủ. Người dùng
(vai điều phối dự án chạy thử) đã được cảnh báo lại và **khẳng định muốn tiếp tục**.
> **DEC-23 (SA)** — Viết `ADR-013` và sửa nhất quán tối thiểu `SAD §6.1`/`§8` + `FAIL §1.2` cho
> e-commerce trong khi AG1 chưa ký thật và AG2 chưa tới hạn — tiếp nối `DEC-01…22`.
> **Quyết định:** Hiện thực hoá Lựa chọn B đã chọn ở `OQ-034` (`DEC-22`) thành một `ADR` chính
> thức, mô tả đầy đủ luồng bất đồng bộ hoá khởi tạo thanh toán, và sửa `SAD §6.1` (sequence
> diagram checkout) + `SAD §8` (bảng ADR) + `FAIL §1.2` (ngân sách timeout cộng dồn) cho khớp với
> quyết định này. Không sửa nội dung khác của `SAD`/`FAIL` ngoài phạm vi được giao. Giữ
> `Confidence 🔴` cho tới khi: (a) AG1/AG2 ký thật, (b) Tech Lead + PO thật xác nhận `ADR-013`
> (không chỉ điều phối dự án ký thay), (c) BA cập nhật `SRS`/`AC` cho US-004 Checkout theo trạng
> thái mới, (d) `ICD` lượt sau chốt endpoint/cơ chế FE lấy `redirectUrl` (polling hay SSE —
> `OQ-041` mới).
> **Người quyết:** Điều phối dự án (đại diện PO, dự án chạy thử) — tiếp tục hoạt động dưới ngoại
> lệ; bản thân nội dung `ADR-013` vẫn cần Tech Lead + PO **thật** ký trước khi `Accepted`.
> **Radar (ước lượng cho việc tiếp tục dưới ngoại lệ, khác điểm radar 7/10 của chính `ADR-013`
> ở trên):** ~4 — chi phí đảo ngược thấp (đây là việc ghi tài liệu, không phải thi công) · bán
> kính ảnh hưởng: `SAD`/`FAIL` đã tồn tại, chỉ sửa đúng 2 mục theo yêu cầu người duyệt · chạm
> `QAS-002` Must gián tiếp (đang sửa lại cách đạt nó, không phải hạ chuẩn) · không ràng buộc dài
> hạn tự thân (khác `ADR-013` — bản thân *việc chạy dưới ngoại lệ* thì sửa được) · không tranh
> cãi mới, tiếp nối tiền lệ `DEC-11…22` → **ghi `DEC-nn`, không cần `ADR` riêng cho việc chạy dưới
> ngoại lệ**.
> **Hệ quả nếu không chấp nhận ngoại lệ:** `OQ-034` tiếp tục treo ở trạng thái "đã chọn hướng B
> nhưng chưa có ADR" — Dev không có tài liệu để thi công lại luồng checkout, `SAD §6.1` và
> `FAIL §1.2` tiếp tục mô tả một luồng đã biết là vượt ngân sách `QAS-002` (~7%) mà không có
> phương án sửa chính thức.
---
## 1. Bối cảnh
`FAIL_e-commerce_v1.0.md §1.2` (bảng ngân sách timeout cộng dồn đường găng checkout) phát hiện:
ngay cả sau khi siết timeout VNPay/Momo xuống 1.500ms (thay 10s kế thừa `CTX §4.2`), tổng thời
gian phản hồi `POST /v1/checkout` ở kịch bản worst-case cộng dồn đạt **3.220ms — vượt `QAS-002`
(≤3.000ms, mức Must) khoảng 7%**. Nguyên nhân gốc: một mình bước "Payment Service gọi VNPay/Momo"
(bước 8 trong bảng ngân sách) đã chiếm phần lớn ngân sách còn lại, trong khi ta **không kiểm soát
được SLA thật của đối tác ngoài** (`CON-04` — SLA VNPay/Momo/GHN/GHTK chưa xác nhận; `ARISK-03` —
chưa có sandbox thật để đo). Đây đúng là tình huống vi phạm `D6`: "timeout tầng ngoài phải lớn
hơn timeout tầng trong" — ở luồng đồng bộ hiện tại, timeout tầng trong (đối tác thanh toán) đang
lớn hơn timeout tầng ngoài (toàn bộ checkout).
`DRV-02` (giỏ hàng ≥2 seller có nguy cơ tỷ lệ bỏ giỏ cao hơn ở bước checkout) là driver kinh doanh
đứng sau — nếu checkout thường xuyên chậm hoặc timeout do đối tác thanh toán, rủi ro bỏ giỏ càng
tăng đúng ở bước cuối cùng, nơi thiệt hại kinh doanh là lớn nhất (khách đã hoàn tất chọn hàng và
tách đơn theo seller).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `QAS-002` | Latency checkout `POST /v1/checkout`: **p95 ≤ 3 giây**, mức **Must** — không đạt thì không được go-live theo định nghĩa `Must` của `sa-2-architecture/SKILL.md` |
| `ASR-004` | Webhook xác nhận thanh toán phải idempotent + có job đối soát bù — ép kiến trúc chấp nhận eventual consistency có giới hạn thời gian cho trạng thái thanh toán; `ADR-013` mở rộng đúng tinh thần này sang cả **bước khởi tạo** thanh toán, không chỉ bước xác nhận |
| `CON-04` | SLA thật của đối tác thanh toán/vận chuyển ngoài chưa được xác nhận bằng hợp đồng |
| `ARISK-03` | Chưa có sandbox thật VNPay/Momo để đo latency init thật — mọi con số timeout hiện tại là đề xuất SA |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Tổng ngân sách worst-case cộng dồn của luồng đồng bộ hiện tại = 3.220ms > 3.000ms | 🟢 Đã kiểm chứng *(bằng tính toán cộng dồn, không phải đo thật)* | `FAIL_e-commerce_v1.0.md §1.2` bảng 10 bước |
| Tách đường găng đồng bộ (bước 1–7, 10) khỏi bước khởi tạo thanh toán (bước 8) đưa tổng thời gian phản hồi xuống ≤3.000ms bất kể VNPay/Momo chậm bao lâu | 🟢 Đã kiểm chứng bằng tính toán (1.670ms + 50ms = 1.720ms, còn dư ~1.280ms so với ngân sách) | Xem bảng §2.3 dưới |
| VNPay/Momo hoàn tất init trong ≤10s ở phần lớn trường hợp khi không còn bị ép timeout ngắn | 🔴 Giả định mới — `ASM-34` | Cần sandbox thật (`ARISK-03`) để xác minh; nếu sai, cửa sổ chờ của FE (§2.2) cần tăng hoặc coi thất bại sớm hơn |
| FE có thể triển khai polling hoặc kênh đẩy (SSE/WebSocket) để lấy `redirectUrl` sau khi có | 🔴 Giả định — chưa có xác nhận năng lực FE hiện tại | Cần Tech Lead FE xác nhận — xem `OQ-041` |
---
## 2. Phương án đã cân nhắc
### PA-A — Giữ đồng bộ, siết timeout hơn nữa
| | |
|---|---|
| **Mô tả** | Giảm timeout gọi VNPay/Momo trong nhánh đồng bộ xuống 1.200ms (thay vì 1.500ms đã đề xuất ở `FAIL v1.0`), đưa tổng ngân sách cộng dồn còn ~2.920ms, có margin ~80ms |
| **Ưu** | Không đổi cấu trúc luồng, không đổi UX FE, không cần state machine mới, không cần BA viết lại `SRS` US-004 |
| **Nhược** | Chỉ là "ép con số" chứ không giải quyết nguyên nhân gốc — ta vẫn không kiểm soát được SLA thật của VNPay/Momo (`CON-04`/`ARISK-03` chưa có sandbox). Nếu đối tác thật cần >1.200ms để phản hồi (rất có thể, vì 10s là con số kế thừa từ hợp đồng gốc, không phải p95 thật đo được), tỷ lệ timeout/thất bại init tăng, khách phải tự bấm "Thử lại" nhiều lần — đúng lúc rủi ro bỏ giỏ hàng cao nhất (`DRV-02`). Margin 80ms cũng rất mỏng, một dao động nhỏ ở bước 1 (network client→ALB, hiện đề xuất 150ms, `ASM-31` chưa đo thật) đã có thể lại vượt ngân sách |
| **Chi phí đảo ngược** | Thấp về mặt kỹ thuật (chỉ đổi số cấu hình) — nhưng rủi ro kinh doanh (bỏ giỏ hàng) không định lượng được trước và có thể đã gây thiệt hại trước khi phát hiện |
### PA-B — Bất đồng bộ hoá bước khởi tạo thanh toán *(chọn)*
| | |
|---|---|
| **Mô tả** | Tách bước gọi VNPay/Momo ra khỏi đường găng đồng bộ của `POST /v1/checkout`. `Order`/`OrderSeller` được tạo và commit ở trạng thái chờ, response trả về ngay cho client trong ngân sách đồng bộ (~1.720ms, xem §2.3); việc gọi VNPay/Momo và lấy `redirectUrl` diễn ra sau, bất đồng bộ. FE lấy `redirectUrl` qua một cơ chế riêng (polling hoặc kênh đẩy — `OQ-041`). COD không đổi (không phụ thuộc bước này) |
| **Ưu** | `QAS-002` được đảm bảo về mặt cấu trúc — không còn phụ thuộc vào SLA của bên thứ ba ta không kiểm soát được, đúng tinh thần `D6`. Hệ quả phụ tích cực: TX DB không còn phải giữ mở xuyên qua lời gọi mạng tới Payment Service (giải quyết một phần mối lo bulkhead đã nêu ở `OQ-037`/`OQ-038` của `FAIL`) |
| **Nhược** | Đổi UX checkout đáng kể (khách không nhận `redirectUrl` ngay, cần chờ/poll); thêm độ phức tạp state machine cho `Order`; cần BA viết lại `SRS`/`AC` cho US-004; cần `ICD` lượt sau chốt endpoint mới; cần xử lý "Order kẹt ở trạng thái chờ" nếu bước async không bao giờ hoàn tất (tương tự khoảng trống outbox đã ghi ở `FAIL §7`) |
| **Chi phí đảo ngược** | Cao — >4 tuần nếu phải quay lại đồng bộ sau khi FE đã triển khai polling/kênh đẩy và BA đã cập nhật `SRS` theo trạng thái mới |
### PA-C — Giữ đồng bộ, chấp nhận vượt ngân sách `QAS-002`
| | |
|---|---|
| **Mô tả** | Không sửa gì, chấp nhận p95 lý thuyết worst-case 3.220ms, coi đây là biên an toàn hiếm khi chạm tới trong thực tế (đa số request sẽ nhanh hơn nhiều vì không phải mọi bước đều đồng thời chạm timeout tối đa) |
| **Ưu** | Không tốn công sức thiết kế lại, không đổi UX, không đổi tài liệu |
| **Nhược** | Vi phạm trực tiếp một `QAS` mức **Must** — theo định nghĩa `Must` ("không đạt thì không được go-live") và tiêu chí chặn của `AG2` ("còn interface/QAS nào không đạt mà không có phương án"), đây không phải lựa chọn SA có thẩm quyền tự chấp nhận (`D10`) — chấp nhận rủi ro vi phạm một ràng buộc chất lượng đã cam kết là quyết định của PO/Tech Lead, không phải mặc định giữ nguyên trong im lặng |
| **Chi phí đảo ngược** | Thấp về kỹ thuật, nhưng **không phải là một quyết định** — đây là không quyết định gì cả và để lại một `QAS` Must đã biết là không đạt trôi tới go-live |
### Bảng so sánh
| Tiêu chí *(sinh từ `QAS-002`/`CON-04`/`DRV-02`)* | PA-A | PA-B | PA-C |
|---|---|---|---|
| Đảm bảo `QAS-002` (Must) bất kể SLA đối tác | 🟡 Không chắc chắn (margin mỏng, phụ thuộc giả định 1.200ms) | 🟢 Có (về mặt cấu trúc) | 🔴 Không |
| Không phụ thuộc SLA đối tác ngoài chưa xác nhận (`CON-04`) | 🔴 Vẫn phụ thuộc | 🟢 Độc lập | 🔴 Vẫn phụ thuộc |
| Chi phí thay đổi FE/BA | 🟢 Không đổi | 🔴 Đổi đáng kể | 🟢 Không đổi |
| Rủi ro bỏ giỏ hàng do timeout init (`DRV-02`) | 🟡 Có thể tăng, không định lượng được | 🟢 Loại bỏ nguyên nhân này | 🔴 Không xử lý |
| Đúng thẩm quyền quyết định (`D10`) | ✅ SA có thể đề xuất, PO/Tech Lead chọn | ✅ SA có thể đề xuất, PO/Tech Lead chọn | ❌ Vi phạm Must trong im lặng, không phải lựa chọn hợp lệ để "không làm gì" |
---
## 3. Quyết định
> **Chọn PA-B — Bất đồng bộ hoá bước khởi tạo thanh toán online (VNPay/Momo) trong `POST
> /v1/checkout`.**
**Vì sao:** Đây là cách duy nhất trong ba phương án đảm bảo `QAS-002` (Must) đạt được **về mặt
cấu trúc**, không phụ thuộc vào một SLA đối tác ngoài mà `CON-04`/`ARISK-03` xác nhận là chưa
kiểm chứng được. PA-A chỉ giảm nhẹ rủi ro mà không loại bỏ nguyên nhân gốc; PA-C vi phạm trực
tiếp một ràng buộc Must mà SA không có thẩm quyền tự chấp nhận thay Tech Lead/PO (`D10`).
**Phạm vi áp dụng:** Chỉ áp dụng cho phương thức thanh toán **online qua cổng bên thứ ba** (VNPay,
Momo). **COD không đổi** — vì COD không gọi ra ngoài, xác nhận ngay trong luồng đồng bộ hiện có
(bước 7 cũ), không có nguyên nhân gây vượt ngân sách. Áp dụng cho `POST /v1/checkout` (US-004,
chưa có `SRS`/`AC` chính thức từ BA — xem §5 Việc phát sinh).
### 3.1 Mô tả luồng mới
**Đường găng đồng bộ (không đổi thứ tự, chỉ dừng lại sớm hơn):**
| Bước | Thành phần | Timeout đề xuất *(kế thừa `FAIL §1.2`)* | Cộng dồn |
|---|---|---|---|
| 1 | Client → ALB | 150ms | 150ms |
| 2 | ALB → Cart & Order | 20ms | 170ms |
| 3 | Xác thực/ownership (cache hit) | 50ms | 220ms |
| 4 | Kiểm tra/giữ tồn kho (in-process) | 150ms | 370ms |
| 5 | Áp dụng coupon/điểm (`IF-021`) | 400ms | 770ms |
| 6 | Ghi `Order`/`OrderSeller`/`OrderItem` (TX) — **COMMIT ngay sau bước này**, không giữ TX mở chờ Payment nữa | 300ms | 1.070ms |
| 7 | *(chỉ áp dụng cho COD)* Payment Service xác nhận COD, đồng bộ | 600ms | 1.670ms *(nhánh COD)* |
| 10 | Response serialization + trả về client | 50ms | **1.720ms (nhánh COD)** |
*(nhánh VNPay/Momo dừng đường găng đồng bộ ngay sau bước 6 + bước 10, không đi qua bước 7 đồng
bộ nữa — xem bảng dưới)*
| Bước | Thành phần | Timeout | Cộng dồn (nhánh VNPay/Momo) |
|---|---|---|---|
| 1–6 | *(giống bảng trên)* | — | 1.070ms |
| 10 | Response serialization + trả về client — trả `orderId` + `status: PENDING_PAYMENT_INIT`, **chưa có `redirectUrl`** | 50ms | **1.120ms** |
**Kết luận:** cả hai nhánh đều nằm sâu dưới `QAS-002` (≤3.000ms) — nhánh VNPay/Momo còn dư
~1.880ms margin so với ngân sách, đủ bù đắp mọi sai lệch chưa đo thật ở bước 1/3/4/5 (`ASM-27`,
`ASM-28`, `ASM-31`).
**Bất đồng bộ (sau khi đã trả response, không tính vào `QAS-002`):**
```mermaid
stateDiagram-v2
[*] --> PENDING_PAYMENT_INIT: TX commit (bước 6), response 201 trả về (bước 10)
PENDING_PAYMENT_INIT --> PAYMENT_INIT_READY: Payment Service gọi VNPay/Momo thành công, có redirectUrl
PENDING_PAYMENT_INIT --> PAYMENT_INIT_FAILED: Hết retry / timeout / lỗi từ VNPay/Momo
PAYMENT_INIT_FAILED --> PENDING_PAYMENT_INIT: Khách chọn "thử lại" (retry init, cùng Idempotency-Key)
PAYMENT_INIT_FAILED --> COD: Khách chọn chuyển sang COD (nếu seller/sản phẩm hỗ trợ)
PAYMENT_INIT_READY --> [*]: Khách hoàn tất thanh toán trên trang VNPay/Momo (luồng webhook — ADR-004, không đổi)
```
**Cơ chế trigger bước async:** Cart & Order publish message `PaymentInitRequested` (message group
= `seller_id`, dedupe theo `eventId`, cùng cơ chế SQS FIFO đã có — `ADR-005`) ngay sau khi commit
TX (bước 6), **cùng lúc** với việc publish `OrderPlaced` đã có sẵn (không phải luồng mới hoàn
toàn, tái dùng hạ tầng message backbone hiện có). Payment Service (`CMP-11`) consume message này,
gọi VNPay/Momo với timeout **10s kế thừa nguyên trạng `CTX §4.2`** (không cần ép ngắn nữa, vì
không còn nằm trên đường găng phản hồi người dùng), cập nhật trạng thái, publish
`PaymentInitReady`/`PaymentInitFailed`.
**Idempotency của bước async:** `Idempotency-Key` gốc của `POST /v1/checkout` (`ICD §3.3`,
`ADR-004`) được truyền kèm message `PaymentInitRequested` — nếu Payment Service consume message
này nhiều lần (redelivery do visibility timeout, `ASM-29`), lời gọi VNPay/Momo không bị lặp lại
sau khi đã có `gatewayTransactionRef` (kiểm tra trạng thái hiện tại trước khi gọi lại — check-
then-act, cùng nguyên tắc đã áp dụng cho GHN/GHTK ở `FAIL FM-03/04`).
**Timeout tổng thể của giai đoạn async (góc nhìn UX, không phải ràng buộc kiến trúc cứng):** đề
xuất coi là "quá lâu, cần hành động" nếu sau **15 giây** kể từ khi tạo `Order` mà vẫn ở
`PENDING_PAYMENT_INIT` (10s timeout đối tác + margin xử lý/hàng đợi) — khi đó FE nên chuyển sang
trạng thái cho phép khách chủ động thao tác (xem §3.2) thay vì chờ vô thời hạn. Đây là con số đề
xuất SA, gắn `OQ-042` (thời gian giữ `Order` ở `PENDING_PAYMENT_INIT` trước khi coi là "kẹt" và
cần job dọn dẹp/hủy tự động).
### 3.2 Cơ chế FE lấy `redirectUrl` — đề xuất, chưa chốt
Hai lựa chọn kỹ thuật cho FE nhận biết khi `PAYMENT_INIT_READY` sẵn sàng:
| Cơ chế | Mô tả | Đánh đổi |
|---|---|---|
| **Polling** *(đề xuất mặc định)* | FE gọi lặp lại một endpoint trạng thái (đề xuất `GET /v1/orders/{orderId}/payment-init-status`, chưa chốt tên — `ICD` lượt sau) mỗi ~1 giây, tối đa ~15 giây | Đơn giản, không cần hạ tầng mới (WebSocket/SSE), hoạt động tốt qua mọi proxy/CDN hiện có; tốn thêm request lặp lại (tải nhỏ, ngắn hạn, không đáng kể so với `QAS-009`) |
| **SSE / WebSocket** | Server đẩy sự kiện khi trạng thái đổi | Trải nghiệm mượt hơn (không có độ trễ polling interval), nhưng cần thêm thành phần hạ tầng (kết nối lâu dài, cân bằng tải phù hợp), tăng độ phức tạp vận hành mà đội chưa xác nhận có kinh nghiệm (`OQ-010` vẫn mở) |
🔴 **Đây là đề xuất, không phải quyết định cuối** — `ADR-013` chỉ chốt việc **bất đồng bộ hoá**,
không chốt cơ chế truyền tải cụ thể. Ghi `OQ-041` (mới), chờ Tech Lead (kèm ý kiến FE) xác nhận;
hệ quả phương án ngược: chọn polling mà tải cao bất thường có thể tăng số request nhỏ không cần
thiết (giảm thiểu bằng interval hợp lý + dừng poll sau 15s hoặc khi có kết quả); chọn SSE/WebSocket
mà đội chưa có kinh nghiệm vận hành production kênh kết nối lâu dài có thể phát sinh sự cố khó
chẩn đoán đúng lúc ra mắt.
### 3.3 Hành vi khi khởi tạo thất bại (`PAYMENT_INIT_FAILED`)
Khi hết retry / timeout ở bước async (VNPay/Momo không phản hồi được trong 10s, hoặc trả lỗi):
`Order` chuyển `PAYMENT_INIT_FAILED`. FE (qua polling/kênh đẩy) nhận trạng thái này và cho khách
hai lựa chọn, **không tự động rollback/huỷ `Order`**:
1. **Thử lại cùng phương thức** — gọi lại endpoint khởi tạo (đề xuất tái dùng cùng
`Idempotency-Key`, `ICD` lượt sau chốt contract cụ thể) → quay lại `PENDING_PAYMENT_INIT`.
2. **Chuyển sang COD** (nếu seller/sản phẩm trong đơn hỗ trợ COD) hoặc **chọn phương thức online
khác** (VNPay ⇄ Momo) — tái sử dụng cùng `Order`/`OrderSeller` đã tạo, chỉ đổi phương thức
thanh toán, không tạo `Order` mới (tránh trùng đơn, giữ đúng ownership tồn kho đã reserve ở
bước 4).
🔴 Chi tiết endpoint/contract cho hai thao tác trên **chưa chốt** — thuộc hoạt động `icd` lượt
sau (xem §5 Việc phát sinh).
---
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-A (siết timeout xuống 1.200ms, giữ đồng bộ) | Không loại bỏ nguyên nhân gốc (phụ thuộc SLA đối tác chưa xác nhận, `CON-04`); margin còn lại (~80ms) quá mỏng so với các con số chưa đo thật khác trong cùng ngân sách (`ASM-27/28/31`) | `CON-04`, `ARISK-03` | Nếu có sandbox thật và VNPay/Momo xác nhận SLA hợp đồng ≤1.000ms p99, có thể xét lại PA-A như một tối ưu bổ sung (không nhất thiết thay thế PA-B, có thể kết hợp: PA-B đã chọn + timeout đối tác chặt hơn nếu SLA cho phép) |
| PA-C (giữ đồng bộ, chấp nhận vượt `QAS-002`) | Vi phạm trực tiếp một `QAS` mức Must mà SA không có thẩm quyền tự chấp nhận thay Tech Lead/PO (`D10`); không phải "không làm gì" là an toàn — đây là để một rủi ro đã biết trôi tới go-live trong im lặng | `QAS-002` (Must) | Chỉ xét lại nếu PO/Tech Lead **chính thức** hạ mức `QAS-002` từ Must xuống Should/Could kèm lý do nghiệp vụ — đây là quyết định PO, không phải kỹ thuật |
*"Giữ đồng bộ vì đơn giản hơn cho FE" là lý do có thật (chi phí thi công thấp hơn) — nhưng không
đủ để thắng một ràng buộc Must đã lượng hoá; ghi nhận ở đây để không ai đề xuất lại PA-C mà không
đọc lý do đã bị loại.*
---
## 5. Hệ quả
**Hệ quả tích cực**
- `QAS-002` (Must) được đảm bảo về mặt cấu trúc cho mọi request VNPay/Momo — không còn phụ thuộc
SLA đối tác ngoài mà ta không kiểm soát (`CON-04`/`ARISK-03`), đúng nguyên tắc `D6`.
- Loại bỏ hoàn toàn tình huống giữ transaction DB mở xuyên qua một lời gọi mạng ra ngoài container
(TX nay commit ngay sau bước 6) — giảm trực tiếp rủi ro cạn connection pool đã nêu ở `FAIL
OQ-037`/`OQ-038`, dù không đóng hẳn hai `OQ` đó (bulkhead vẫn cần cho các phụ thuộc khác).
- Payment Service có thể dùng lại nguyên trạng timeout 10s kế thừa `CTX §4.2` cho VNPay/Momo mà
không cần "ép" xuống 1.500ms như đề xuất tạm ở `FAIL v1.0` — giảm rủi ro timeout giả (false
timeout) khi đối tác chỉ chậm tạm thời.
**Hệ quả tiêu cực phải sống chung**
- UX checkout đổi đáng kể: khách không nhận `redirectUrl` ngay trong response đầu tiên, cần chờ
một khoảng thời gian ngắn (kỳ vọng thường vài trăm ms tới vài giây, tối đa đề xuất 15s trước khi
coi là bất thường) — cần màn hình chờ/trạng thái mới ở FE, chưa thiết kế ở lượt này.
- Thêm độ phức tạp: state machine 4 trạng thái mới cho `Order` (`PENDING_PAYMENT_INIT` →
`PAYMENT_INIT_READY` / `PAYMENT_INIT_FAILED` → …), thêm 1 loại message
(`PaymentInitRequested`) cần DLQ/retry riêng (tương tự các message khác đã có ở `ADR-005`).
- Khoảng trống chưa xử lý: nếu message `PaymentInitRequested` bị mất hoặc Payment Service không
bao giờ xử lý (sự cố), `Order` kẹt vĩnh viễn ở `PENDING_PAYMENT_INIT` — cần job quét dọn (cùng
bản chất với khoảng trống outbox đã ghi ở `FAIL §7`, chưa thiết kế chi tiết, thuộc hoạt động
`dat`/`inf` sắp tới). Ghi `OQ-042`.
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Quay lại luồng đồng bộ hoàn toàn (bỏ polling/kênh đẩy) | Thiết kế lại FE (bỏ màn hình chờ), viết `ADR` mới `Supersedes: ADR-013`, cập nhật lại `SRS` US-004 lần nữa, và **chỉ khả thi nếu** có sandbox thật chứng minh SLA đối tác đủ nhanh (`ARISK-03` đã đóng) |
| Đổi cơ chế polling ↔ SSE/WebSocket (sau khi đã chọn một trong hai ở `OQ-041`) | Đổi tầng truyền tải FE↔BE, không đổi state machine `Order` — chi phí trung bình, không cần `ADR` mới (thuộc `ICD`, không phải quyết định kiến trúc cấu trúc) |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Cập nhật `SAD §6.1` (sequence diagram checkout) + `§8` (bảng ADR) theo `ADR-013` | SA | Đã làm cùng lượt (`SAD` v1.3) | `SAD_e-commerce_v1.0.md` |
| Cập nhật `FAIL §1.2` (ngân sách timeout cộng dồn, tách đường găng đồng bộ khỏi bước async) | SA | Đã làm cùng lượt (`FAIL` v1.1) | `FAIL_e-commerce_v1.0.md` |
| Chốt endpoint/contract: trạng thái polling, retry init, chuyển COD | SA (hoạt động `icd` lượt sau) | Trước khi Dev thi công US-004 | `ICD_e-commerce_v1.0.md` (bổ sung `IF` mới) |
| Viết/cập nhật `SRS`/`AC` cho US-004 Checkout với trạng thái `PENDING_PAYMENT_INIT`/`PAYMENT_INIT_READY`/`PAYMENT_INIT_FAILED` | BA | Trước khi Dev thi công | `ba-output/e-commerce/03-specification/SRS_US004_*.md` (chưa tồn tại) |
| Thiết kế job quét `Order` kẹt ở `PENDING_PAYMENT_INIT` quá hạn (`OQ-042`) | SA (hoạt động `dat`/`inf` lượt sau) | Trước AG2 | `DAT_e-commerce_v1.0.md` (chưa tồn tại) |
| Thêm fitness function đo p95 checkout thật ≤3s bất kể VNPay/Momo chậm | SA/QA (GĐ3) | GĐ3 | `FIT-23` (mới, xem §6) |
---
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Đo p95 `POST /v1/checkout` (nhánh VNPay/Momo) ≤3.000ms **ngay cả khi** giả lập VNPay/Momo phản hồi chậm (chaos, ép độ trễ 9–10s ở bước async) — xác nhận response không bị ảnh hưởng | k6 + chaos injection (giả lập độ trễ đối tác) | Staging | `FIT-23` *(mới, cộng thêm vào danh sách `FIT-16…22` đã đề xuất ở `FAIL §8`)* |
| Xác nhận không có `Order` nào giữ TX DB mở quá bước 6 (kiểm tra thời lượng transaction qua log/APM) | Kiểm tra APM/log transaction duration | Staging + production sau go-live | `FIT-24` *(mới)* |
| Xác nhận message `PaymentInitRequested` idempotent — gửi trùng không gọi lại VNPay/Momo khi đã có `gatewayTransactionRef` | Test tích hợp: publish trùng message, xác nhận chỉ 1 lời gọi gateway | Staging | `FIT-25` *(mới)* |
Cả ba `FIT-23/24/25` là **ứng viên**, chưa viết thật (thuộc GĐ3 `sa-3-enablement`, cùng nhóm với
`FIT-16…22` đã đề xuất ở `FAIL §8`) — không giả vờ đã kiểm chứng.
---
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Tỷ lệ `Order` rơi vào `PAYMENT_INIT_FAILED` sau go-live | > 5% tổng số checkout VNPay/Momo trong 1 tuần (đề xuất, chưa xác nhận — cần baseline thật sau go-live) | Tech Lead + PO |
| Thời gian trung bình từ `PENDING_PAYMENT_INIT` → `PAYMENT_INIT_READY` | > 5 giây p95 (gần chạm ngưỡng "kẹt" 15s ở §3.1) | SRE |
| Sandbox VNPay/Momo thật cho thấy SLA p99 ổn định ≤ 1.000ms | Có báo cáo SLA chính thức từ đối tác | Tech Lead (đóng `ARISK-03`) |
| Phản hồi người dùng về UX chờ/poll (khảo sát/CSKH) cho thấy trải nghiệm xấu đáng kể | Ngưỡng định tính, cần PO đánh giá | PO |
**Điều kiện chuyển `Accepted`:** Tech Lead + PO **thật** (không phải ký thay dưới ngoại lệ
`DEC-01`) xác nhận nội dung §3 (mô tả luồng), đồng thời BA xác nhận đã cập nhật `SRS`/`AC` cho
US-004 theo trạng thái mới. Radar 7/10 (dưới ngưỡng 8) nên không bắt buộc POC trước `Accepted`,
nhưng khuyến nghị mạnh chạy `FIT-23` (chaos đo p95) ít nhất một lần ở staging trước khi go-live,
vì đây là ràng buộc chất lượng mức Must.
---
## 8. Tham chiếu
- POC: *(không bắt buộc — radar 7/10, dưới ngưỡng 8)*
- Bài đo: `FIT-23/24/25` (ứng viên, chưa chạy — xem §6)
- Tài liệu ngoài: `CTX_e-commerce_v1.0.md §4.2` (timeout đối tác kế thừa 10s)
- Thảo luận: `FAIL_e-commerce_v1.0.md §1.2` (phát hiện xung đột ngân sách), `OQ-034`/`DEC-22`
(quyết định chọn Lựa chọn B, 2026-09-15, điều phối dự án thay mặt Tech Lead/PO)
---
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung §3 (state machine `PENDING_PAYMENT_INIT`/`PAYMENT_INIT_READY`/`PAYMENT_INIT_FAILED`, timeout async 10s, idempotency, hành vi thất bại) đủ làm cơ sở thiết kế tiếp (`ICD`/`DAT`/FE) | Chưa có chữ ký thật Tech Lead + PO (`§7` điều kiện `Accepted`); BA chưa viết `SRS`/`AC` cho US-004 (`OQ-034`) |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" (mục #9) cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review:
> 🔴 — `AG2` chưa ký.

View File

@@ -0,0 +1,207 @@
# ADR-014 — Transactional Outbox cho publish sự kiện Cart & Order / Payment
*Tên file: `adr/ADR-014_transactional-outbox-publish-su-kien.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead (cấu trúc/tích hợp — `decision-radar.md §5`) — chưa ký thật |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **6/10** *(chấm theo `decision-radar.md §2`, kế thừa nguyên ước lượng đã ghi ở `DAT_e-commerce_v1.0.md §4.1`: Chi phí đảo ngược=1 (1–3 tuần — `outbox_event` là bảng nội bộ, không phải dữ liệu nghiệp vụ, chưa tới mức "phải migrate dữ liệu") · Bán kính ảnh hưởng=1 (Cart&Order + Payment, 2 `CMP` publish, cộng tiền lệ cho các module tương lai) · Chạm `QAS` Must=1 (gián tiếp `QAS-005`/`DRV-08` — không mất giao dịch, không phải chính nó là con số `QAS`) · Ràng buộc dài hạn=2 (trở thành chuẩn chung cho mọi module publish sự kiện, ≥1 năm) · Tranh cãi=0, cộng +1 vì đây là quyết định **thiết lập tiền lệ** áp dụng toàn hệ thống (`decision-radar.md §4`) → **5–7 ⇒ `ADR` bắt buộc**, dưới ngưỡng 8 nên không bắt buộc POC trước khi `Accepted`, nhưng cần bài đo/`FIT` xác nhận trước khi chuyển `Accepted` theo §6)* |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-004` · `ASR-005` · `QAS-005` · `DRV-08` · `ADR-004` · `ADR-005` · `ADR-013` · `DAT_e-commerce_v1.0.md §1.3, §1.4, §4.1` · `FAIL_e-commerce_v1.0.md §7` · `OQ-044` · `DEC-25` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-014` và đổi trạng thái bản cũ thành `Superseded by`. Không sửa nội dung ADR
> đã `Accepted`, không xoá ADR đã `Rejected`.
---
## 0. Ghi chú Preflight & ngoại lệ gate — bắt buộc đọc trước
AG1 vẫn chưa ký thật; AG2 (gate của chính GĐ2) chưa tới hạn. Theo `SKILL.md`, hoạt động 1 (`QAS`)
→ 2 (`ASR`) → 3 (`SAD`) đã hoàn tất và được duyệt từng phần từ trước; hoạt động 4–8 (`ICD`/`DAT`/
`SEC`/`INF`/`FAIL`) đã chạy và duyệt từng phần (`DEC-18…30`). `ADR-001…013` đã viết (`Proposed`).
`DAT_e-commerce_v1.0.md §4.1` (duyệt từng phần, `DEC-25`) đã chọn **TẠM** hướng Transactional
Outbox cho khoảng trống dual-write phát hiện ở `FAIL §7`, và yêu cầu SA viết `ADR-014` chính thức
ở lượt hoạt động `adr` này. Người dùng (vai điều phối dự án chạy thử) đã được cảnh báo lại và
**khẳng định muốn tiếp tục**.
> **DEC-31 (SA)** — Viết `ADR-014`/`ADR-015`/`ADR-016`/`ADR-017` (`Proposed`) cho e-commerce trong
> khi AG1 chưa ký thật và AG2 chưa tới hạn — tiếp nối `DEC-01…30`. Hiện thực hoá 4 hướng đã chọn
> TẠM qua `OQ-044`/`DEC-25` (outbox), `OQ-050`/`DEC-28` (service JWT), và 2 ứng viên mới của `INF`
> (`DEC-30`: blue-green, Terraform). Tất cả 4 `ADR` giữ `Status Proposed` — không `Accepted`. Chỉ
> link 4 `ADR` này vào `DAT §4.1`, `SEC §2.4`/§4 (TB3/TB4), `INF §7.2`/§11 (thay "ứng viên"/"chưa
> viết" bằng đường dẫn thật), không đổi nội dung chuyên môn khác của ba tài liệu đó — mỗi file
> chạm tới bump `+0.1` kèm Change Log "chỉ link ADR, không đổi nội dung". Cập nhật `ADL`/`DTM`.
> **Người quyết:** Điều phối dự án (đại diện PO, dự án chạy thử).
> **Radar (ước lượng cho việc tiếp tục dưới ngoại lệ, khác điểm radar riêng của từng ADR):** ~4 —
> chi phí đảo ngược thấp (ghi 4 ADR + link tài liệu, chưa có dòng code phụ thuộc) · bán kính ảnh
> hưởng: AG2 phụ thuộc nhưng bản thân *việc ghi dưới ngoại lệ* thì nhỏ · chạm gián tiếp `QAS-005`/
> `QAS-007`/`QAS-002`/`QAS-010` Must (đang hiện thực hoá các cơ chế đạt chúng) nhưng chưa cam kết
> thi công (không `ADR` nào `Accepted`) · không ràng buộc dài hạn tự thân (nội dung từng `ADR` đã
> được chấm radar riêng, 5–7, không cái nào ≥8) · không tranh cãi mới, tiếp nối tiền lệ `DEC-17/23`
> → **ghi `DEC-nn`, không cần thêm `ADR` cho chính việc chạy dưới ngoại lệ**.
> **Hệ quả nếu không chấp nhận ngoại lệ:** `OQ-044`/`OQ-050` tiếp tục treo ở trạng thái "đã chọn
> hướng nhưng chưa có `ADR`" — Dev không có tài liệu chính thức để thi công outbox/service-authn;
> `INF §7.2`/§11 tiếp tục ghi "đề xuất ADR mới, chưa viết", để lộ khoảng trống ở tiêu chí AG2
> "`ADR` tồn tại cho mọi quyết định đạt ngưỡng radar".
---
## 1. Bối cảnh
`FAIL_e-commerce_v1.0.md §7` phát hiện một khoảng trống: nếu service chết **sau** `COMMIT`
transaction tạo `Order` nhưng **trước** khi publish `OrderPlaced`/`PaymentInitRequested` thành
công, sự kiện bị mất vĩnh viễn — không consumer nào (Commission, Promotion, Notification,
Shipping, Payment) biết `Order` đó tồn tại. Đây là bài toán **dual write** kinh điển (ghi DB + gọi
message queue không nguyên tử). `DAT_e-commerce_v1.0.md §4.1` đã phân tích khoảng trống này và đề
xuất Transactional Outbox, được duyệt **TẠM** qua `DEC-25` với yêu cầu SA viết `ADR` chính thức.
`ADR-005` đã chốt message backbone (SQS FIFO + EventBridge); `ADR-013` bổ sung một sự kiện mới
(`PaymentInitRequested`) publish **cùng lúc** với `OrderPlaced` ngay sau khi commit — càng làm
tăng tầm quan trọng của việc publish nguyên tử, vì nay có ≥2 sự kiện phải cùng "sống hay chết"
với transaction ghi `Order`.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-004` | Webhook + xác nhận thanh toán phải idempotent + có cơ chế bù — mở rộng tinh thần sang cả bước publish sự kiện tạo đơn |
| `ASR-005` | Message backbone SQS FIFO + EventBridge đã chọn (`ADR-005`) — outbox là cơ chế publish nguyên tử **lên trên** backbone đó, không thay thế nó |
| `QAS-005` | Fan-out lag ≤5 giây từ `OrderPlaced` tới consumer — outbox relay (đề xuất 1–2 giây) phải nằm trong ngân sách này |
| `DRV-08` | Xác nhận thanh toán/giao dịch không được mất — outbox là biện pháp trực tiếp chống mất sự kiện `OrderPlaced`/`PaymentInitRequested` |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Dual-write (ghi DB + gọi queue không cùng transaction) có thể mất sự kiện nếu service chết giữa hai bước | 🟢 Đã kiểm chứng — đây là vấn đề đã biết rộng rãi trong kiến trúc phân tán, không cần đo riêng cho dự án này | `FAIL §7` |
| Relay poller in-process (đọc `outbox_event` mỗi 1–2 giây) đủ nhanh để không ăn hết ngân sách `QAS-005` (≤5 giây) | 🔴 Giả định (`ASM-38`, `DAT §4.1`) | Chưa đo — cần bài đo GĐ3 |
| Consumer đã có dedupe theo `eventId` (chống publish trùng khi relay publish thành công nhưng đánh dấu `published` thất bại — at-least-once) | 🟢 Đã kiểm chứng — đã thiết kế ở `ICD §4` | `ICD_e-commerce_v1.0.md §4` |
## 2. Phương án đã cân nhắc
### PA-1 — Job quét đối chiếu định kỳ
| | |
|---|---|
| **Mô tả** | Job định kỳ (đề xuất mỗi 5 phút) quét `order` không có sự kiện tương ứng đã publish thành công (dựa cột đánh dấu `event_published_at`), publish lại |
| **Ưu** | Không cần bảng/hạ tầng mới, chỉ thêm 1 cột đánh dấu |
| **Nhược** | Không giải quyết nguyên nhân gốc — luồng publish "bình thường" (ngay sau commit) vẫn là dual-write không nguyên tử, job chỉ là lưới vá muộn; có race condition giữa "đang xử lý" và "đã mất" nếu không thiết kế cẩn thận cột đánh dấu; độ trễ phát hiện tối đa bằng chu kỳ job (5 phút) — vượt xa ngân sách `QAS-005` (≤5 giây) cho trường hợp cần vá; không tổng quát — mỗi module publish event sau này phải tự cài lại logic quét riêng |
| **Chi phí đảo ngược** | Trung bình — đổi sang cơ chế khác sau khi nhiều module đã cài job quét riêng tốn công dọn dẹp từng nơi |
### PA-2 — Transactional Outbox *(chọn)*
| | |
|---|---|
| **Mô tả** | Ghi một dòng `outbox_event` **trong cùng transaction DB** với việc tạo `Order`/`OrderSeller`/`OrderItem` (cùng schema `cart_order`, cùng ACID). Một relay (poller in-process, đề xuất chu kỳ 1–2 giây, `ASM-38`) đọc `outbox_event` trạng thái `pending`, publish lên SQS/EventBridge, đánh dấu `published` khi thành công. Payment Service dùng bảng `outbox_event` riêng trong RDS Payment của nó (2 DB khác nhau, không chia sẻ bảng) |
| **Ưu** | Đảm bảo tính nguyên tử thật sự giữa ghi DB và phát sự kiện (industry-standard); dùng được cho **mọi** module publish event sau này, không chỉ Cart & Order; độ trễ phát hiện/publish thấp (1–2 giây), nằm gọn trong ngân sách `QAS-005` (≤5 giây) |
| **Nhược** | Thêm bảng `outbox_event` mỗi module publish + một relay process cần giám sát riêng (lag alerting); độ trễ publish tăng nhẹ (1–2 giây thay vì tức thời) |
| **Chi phí đảo ngược** | Trung bình (1–3 tuần) — sau khi Cart&Order + Payment đã tích hợp, đổi sang cơ chế khác cần sửa lại relay + rà lại mọi module đã dùng, nhưng `outbox_event` là bảng nội bộ kỹ thuật, không phải dữ liệu nghiệp vụ, nên chưa tới mức "phải migrate dữ liệu" |
### PA-3 — 2PC (Distributed transaction giữa DB và message broker)
| | |
|---|---|
| **Mô tả** | Dùng giao thức 2-phase commit (XA transaction) trải rộng cả việc ghi DB lẫn publish message, đảm bảo cả hai cùng commit hoặc cùng rollback |
| **Ưu** | Về lý thuyết là đảm bảo nhất quán mạnh nhất giữa hai hệ thống |
| **Nhược** | **Không khả thi kỹ thuật** — SQS FIFO và EventBridge (đã chốt ở `ADR-005`) là dịch vụ managed của AWS, **không hỗ trợ tham gia giao thức XA/2PC**; ngay cả nếu đổi sang một message broker hỗ trợ 2PC, cơ chế này làm giảm throughput đáng kể (transaction coordinator là điểm nghẽn tập trung) và đội chưa có kinh nghiệm vận hành (`CON-05`) |
| **Chi phí đảo ngược** | Không áp dụng — loại ngay từ đầu vì không khả thi kỹ thuật với hạ tầng đã chọn |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Đảm bảo nguyên tử ghi DB + publish | ❌ Không (chỉ vá muộn) | ✅ Có | ✅ Về lý thuyết |
| Khả thi với SQS FIFO/EventBridge (`ADR-005`) | ✅ | ✅ | ❌ Không hỗ trợ XA |
| Độ trễ phát hiện/publish khi lỗi | Tối đa chu kỳ job (5 phút) — vượt `QAS-005` | 1–2 giây (`ASM-38`) — trong ngân sách `QAS-005` | N/A (không khả thi) |
| Tổng quát cho mọi module tương lai | 🔶 Phải cài riêng từng nơi | ✅ Một chuẩn chung | N/A |
## 3. Quyết định
> **Chọn PA-2 — Transactional Outbox (bảng `outbox_event` + relay poller in-process chu kỳ
> 1–2 giây).**
**Vì sao:** Đây là phương án duy nhất vừa giải quyết đúng nguyên nhân gốc (dual-write không
nguyên tử) vừa khả thi với message backbone đã chọn (`ADR-005`), và tổng quát hoá được cho mọi
module publish sự kiện trong tương lai — đúng tinh thần `DRV-08` (không mất giao dịch).
**Phạm vi áp dụng:** Toàn hệ thống — mọi module publish sự kiện qua SQS FIFO/EventBridge (bắt đầu
với Cart & Order publish `OrderPlaced`/`PaymentInitRequested`, và Payment Service publish
`PaymentConfirmed`/`PaymentInitReady`/`PaymentInitFailed` qua bảng `outbox_event` riêng trong RDS
Payment của nó). Module nào publish sự kiện mới sau này (Commission, Shipping, …) dùng cùng mẫu.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Tech Lead xác nhận thiết kế relay (poller in-process, chu kỳ 1–2 giây, số instance/HA của relay) | Tech Lead | Mở (`OQ-044` chưa đóng) |
| Bài đo xác nhận relay lag thật không ăn quá nhiều vào ngân sách `QAS-005` (≤5 giây) | Tech Lead + QA | Chưa chạy — GĐ3 |
| `FIT` §6 dưới đây chạy PASS ít nhất 1 lần ở staging | Tech Lead/SRE | Chưa chạy |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Job quét đối chiếu định kỳ | Không giải quyết nguyên nhân gốc (vẫn dual-write ở luồng bình thường), độ trễ phát hiện tối đa 5 phút vượt xa ngân sách `QAS-005` (≤5 giây), không tổng quát cho module tương lai | `QAS-005`, `DRV-08` | Chỉ xét lại như biện pháp bổ sung (không thay thế outbox) nếu cần một lớp giám sát "vá muộn" cho các dòng `outbox_event` bị kẹt bất thường — bản thân outbox relay đã đóng vai trò chính |
| PA-3 — 2PC (XA transaction) | Không khả thi kỹ thuật — SQS FIFO/EventBridge không hỗ trợ XA/2PC (`ADR-005`); ngay cả đổi broker, giảm throughput đáng kể và đội chưa có kinh nghiệm (`CON-05`) | `ADR-005`, `CON-05` | Chỉ xét lại nếu đổi hẳn message backbone sang một hệ có hỗ trợ 2PC **và** có bằng chứng throughput đáp ứng — chưa có tiền lệ ở dự án này |
## 5. Hệ quả
**Hệ quả tích cực**
- Đảm bảo tính nguyên tử thật sự giữa ghi `Order` và phát sự kiện — đóng khoảng trống `FAIL §7`
- Một chuẩn chung dùng được cho mọi module publish sự kiện sau này (Commission, Shipping,
Notification, …), không phải thiết kế lại từ đầu mỗi lần
- Cho phép `ADR-013` publish đồng thời `OrderPlaced` + `PaymentInitRequested` một cách an toàn
**Hệ quả tiêu cực phải sống chung**
- Thêm bảng `outbox_event` ở mỗi module publish (Cart & Order, Payment Service — đã thiết kế ở
`DAT §1.3`/§1.4) — tăng độ phức tạp schema
- Relay process là một thành phần mới cần giám sát riêng (lag alerting theo tuổi dòng `pending`,
chưa thiết kế ngưỡng cụ thể — thuộc `inf`)
- Độ trễ publish tăng nhẹ (1–2 giây thay vì tức thời) — cần xác nhận không ăn quá nhiều vào ngân
sách `QAS-005` (≤5 giây) khi cộng cả thời gian consumer xử lý
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi khỏi Outbox sang cơ chế khác (VD CDC — Change Data Capture) sau khi mọi module đã tích hợp | Cần thay relay + rà lại từng module đã dùng — nhiều tuần, dù không phải migrate dữ liệu nghiệp vụ |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Thiết kế chi tiết relay (số instance, HA, cơ chế lock tránh 2 relay cùng publish 1 dòng) | Tech Lead + SA (hoạt động `inf` lượt sau) | Trước khi Dev thi công | `INF_e-commerce_v1.0.md` (bổ sung) |
| Thêm alerting lag `outbox_event` (tuổi dòng `pending` vượt ngưỡng) | SA (hoạt động `inf`)/Ops | Trước AG2 | `INF_e-commerce_v1.0.md` |
| Viết `FIT-26`/`FIT-27` thật | QA/Dev (GĐ3) | GĐ3 | `03-enablement/FIT_e-commerce.md` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Không có publish nào đi thẳng SQS/EventBridge mà bỏ qua ghi `outbox_event` trong cùng transaction ("không publish ngoài outbox") | Static analysis (lint rule cấm gọi trực tiếp SDK publish ngoài lớp outbox) + integration test kiểm tra mọi `Order` tạo ra đều có dòng `outbox_event` tương ứng trong cùng transaction | CI + staging | `FIT-26` *(ứng viên GĐ3)* |
| Chaos: kill relay process giữa lúc có tải, xác nhận `outbox_event` tồn đọng không mất; khi relay phục hồi (restart), toàn bộ dòng `pending` được publish, không mất và không publish thiếu | Chaos test — dừng relay, tạo N `Order`, khởi động lại relay, đếm số sự kiện consumer nhận được so với N | Staging | `FIT-27` *(ứng viên GĐ3)* |
Cả `FIT-26`/`FIT-27` là **ứng viên**, chưa viết thật (thuộc GĐ3 `sa-3-enablement`).
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Relay lag thật (từ lúc `outbox_event` ghi tới lúc publish) thường xuyên vượt ngưỡng, ăn quá nhiều vào ngân sách `QAS-005` | p95 lag > 3 giây (margin còn lại quá mỏng so với ngân sách 5 giây) | Tech Lead + SRE |
| Số dòng `outbox_event` tồn đọng (`pending`) tăng liên tục, không giảm | Tồn đọng > vài phút liên tục theo dõi qua CloudWatch (ngưỡng cụ thể chưa chốt — thuộc `inf`) | Ops/SRE |
## 8. Tham chiếu
- POC: Không bắt buộc (radar 6/10, dưới ngưỡng 8) — nhưng khuyến nghị bài đo relay lag trước khi
`Accepted`
- Bài đo: `FIT-26`/`FIT-27` (ứng viên GĐ3)
- Tài liệu ngoài: —
- Thảo luận: `DAT_e-commerce_v1.0.md §4.1` (phát hiện + đề xuất), `FAIL_e-commerce_v1.0.md §7`
(khoảng trống gốc), `OQ-044`/`DEC-25` (chọn hướng TẠM)

View File

@@ -0,0 +1,185 @@
# ADR-015 — Xác thực service-to-service bằng Service JWT ngắn hạn
*Tên file: `adr/ADR-015_service-jwt-xac-thuc-service-to-service.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | Security (chưa có người — `OQ-007`) + Tech Lead (bảo mật — `decision-radar.md §5`; Security có quyền phủ quyết cuối cùng) — chưa ký thật |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **5/10** *(chấm theo `decision-radar.md §2`, kế thừa nguyên ước lượng đã ghi ở `SEC_e-commerce_v1.0.md §2.4`: Chi phí đảo ngược=1 (1–3 tuần — đổi cơ chế sau khi đã code tốn sửa cả hai đầu gọi) · Bán kính ảnh hưởng=2 (mọi lời gọi cross-network nội bộ TB3/TB4 + tiền lệ cho các `IF` tương lai) · Chạm `QAS` Must=1 (gián tiếp `QAS-002`/`QAS-010` — thêm bước xin token có thể ăn vào ngân sách latency đã eo hẹp, cần đo lại) · Ràng buộc dài hạn=1 (chuẩn nội bộ cho service-to-service, trong một release) · Tranh cãi=0 → **5 ⇒ `ADR` bắt buộc** (5–7), dưới ngưỡng 8 nên không bắt buộc POC, nhưng cần đo lại latency overhead trước khi `Accepted`)* |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-002` · `ASR-007` · `QAS-002` · `QAS-010` · `THR-09` · `THR-13` · `SEC_e-commerce_v1.0.md §1 (TB3/TB4)`, `§2.4` · `SAD_e-commerce_v1.0.md §7` (ràng buộc không thêm hạ tầng mới) · `ADR-002` · `ADR-007` · `ADR-012` · `OQ-050` · `DEC-28` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-015` và đổi trạng thái bản cũ thành `Superseded by`. Không sửa nội dung ADR
> đã `Accepted`, không xoá ADR đã `Rejected`.
---
## 0. Ghi chú Preflight & ngoại lệ gate
Viết cùng lượt với `ADR-014`/`ADR-016`/`ADR-017` dưới ngoại lệ gate ghi ở `DEC-31` (tiếp nối
`DEC-01…30`) — xem `ADR-014 §0` cho bối cảnh đầy đủ. Riêng `ADR-015`: hiện thực hoá hướng **PA-3**
đã chọn TẠM ở `SEC_e-commerce_v1.0.md §2.4` (duyệt từng phần, `DEC-28`).
🔴 **Lưu ý quan trọng nhất của ADR này:** đây là quyết định **bảo mật** — theo `decision-radar.md
§5`, loại quyết định này do **Security chốt**, không phải Tech Lead hay SA. Security **hiện chưa
có người được chỉ định** (`OQ-007`, mở từ GĐ1). ADR này **không thể chuyển `Accepted`** cho tới
khi có đại diện Security thật ký — bất kể Tech Lead có đồng ý nội dung hay không, đúng nguyên tắc
"Security có quyền phủ quyết AG2" (`sa-2-architecture/SKILL.md`, hoạt động 6). Người dùng (vai
điều phối dự án chạy thử) đã được cảnh báo lại và **khẳng định muốn tiếp tục** dưới ngoại lệ.
---
## 1. Bối cảnh
`SEC_e-commerce_v1.0.md §1`/`§2.4` phát hiện một khoảng trống: hai ranh giới cross-network nội bộ
— TB3 (Nhóm Giao dịch ↔ Nhóm Hỗ trợ, `IF-021`/`IF-022`) và TB4 (Cart & Order → Payment, `IF-006`,
**ranh giới nhạy cảm nhất vì Payment giữ tiền thật**, `ASR-002`) — hiện **chỉ dựa vào network
reachability** (security group VPC), không có cơ chế xác thực danh tính bên gọi. `THR-09` và
`THR-13` (Spoofing, mức 🔴) ghi rõ: một service khác trong cùng VPC (kể cả bị compromise) có thể
giả danh gọi thẳng các endpoint này nếu security group cấu hình sai.
`SAD_e-commerce_v1.0.md §7` (v1.2) đã đặt một ràng buộc rõ ràng: **không tự thêm hạ tầng mới**
(VD internal ALB/App Mesh) vào sơ đồ ở lượt thiết kế này — mọi hạ tầng mới là một quyết định kiến
trúc riêng, ngoài phạm vi. Điều này loại trực tiếp phương án service mesh/mTLS đầy đủ trừ khi có
một `ADR` riêng chấp nhận thêm hạ tầng.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-002` | Payment Service cô lập PCI-DSS SAQ A — TB4 là ranh giới nhạy cảm nhất |
| `ASR-007` | Chỗ ra quyết định ownership/authz — mở rộng tinh thần sang cả xác thực giữa các service nội bộ |
| `SAD §7` (v1.2) | Không tự thêm hạ tầng mới ngoài phạm vi thiết kế hiện tại |
| `CON-05` | Đội vận hành chưa xác nhận có kinh nghiệm vận hành hạ tầng phân tán phức tạp (service mesh) |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Hạ tầng ký/verify JWT của Identity & Access (`CMP-02`) đã tồn tại cho xác thực người dùng cuối | 🟢 Đã kiểm chứng | `SEC §2` |
| Thêm 1 bước xin/verify service token không ăn quá nhiều vào ngân sách latency `QAS-002`/`QAS-010` | 🔴 Giả định mới — cần đo lại | Chưa đo — GĐ3 |
| AWS Cloud Map + ECS Service Connect (đã chốt ở `INF §3`, mức `DEC`, không `ADR`) tương thích với việc đính kèm header service JWT ở tầng ứng dụng | 🟢 Đã kiểm chứng (đây là 2 lớp độc lập — network discovery vs application-layer auth) | `INF §3` |
## 2. Phương án đã cân nhắc
### PA-1 — Chỉ dựa network (security group)
| | |
|---|---|
| **Mô tả** | Giữ nguyên hiện trạng — không thêm cơ chế xác thực nào giữa các service nội bộ |
| **Ưu** | Không tốn công triển khai |
| **Nhược** | Không đáp ứng "defense in depth" cho dữ liệu PII/tiền đi qua TB3/TB4; một security group cấu hình sai = không còn ranh giới nào; không giảm thiểu `THR-09`/`THR-13` |
| **Chi phí đảo ngược** | Thấp về kỹ thuật, nhưng để lại `THR-09`/`THR-13` ở mức 🔴 không giảm thiểu — rủi ro bảo mật tồn đọng |
### PA-2 — mTLS qua service mesh (AWS App Mesh/Private CA)
| | |
|---|---|
| **Mô tả** | Mỗi ECS task có chứng chỉ riêng, xác thực lẫn nhau ở tầng TLS |
| **Ưu** | Mạnh nhất, chuẩn ngành cho zero-trust nội bộ |
| **Nhược** | Thêm hạ tầng mới (App Mesh) — **mâu thuẫn trực tiếp** với ràng buộc đã ghi ở `SAD §7` v1.2 ("không tự thêm hạ tầng mới ngoài phạm vi"); đội chưa xác nhận có kinh nghiệm vận hành mesh (`CON-05`) |
| **Chi phí đảo ngược** | Cao — triển khai rồi tháo dỡ mesh tốn nhiều tuần, ảnh hưởng mọi service |
### PA-3 — Service JWT ngắn hạn ký bởi Identity & Access *(chọn)*
| | |
|---|---|
| **Mô tả** | Mỗi service tự xin "service token" (JWT riêng, `sub=service-name`, TTL ngắn ~5 phút, đề xuất) từ Identity & Access qua IAM role (không cần secret tĩnh), đính kèm header khi gọi `IF-006`/`IF-021`/`IF-022`; bên nhận verify chữ ký giống verify JWT người dùng (tái dùng hạ tầng ký/verify đã có) |
| **Ưu** | Không cần hạ tầng mới ngoài quy ước header + logic verify (tái dùng key JWT của `CMP-02`); tương thích với ràng buộc "không thêm hạ tầng mới" của `SAD §7`; nâng được một lớp phòng thủ so với hiện trạng thuần network |
| **Nhược** | Cần Identity & Access cấp phát token cho service (thêm luồng nội bộ mới, nhỏ); vẫn phụ thuộc network layer làm lớp phòng thủ đầu (không phải mTLS zero-trust đầy đủ); thêm 1 round-trip xin token có thể ăn vào ngân sách latency (cần đo lại) |
| **Chi phí đảo ngược** | Trung bình (1–3 tuần) — đổi cơ chế sau khi đã code cả hai đầu gọi (bên gửi xin token, bên nhận verify) tốn sửa cả hai phía |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Giảm thiểu `THR-09`/`THR-13` (Spoofing) | ❌ Không | ✅ Mạnh nhất | ✅ Có (một lớp phòng thủ bổ sung) |
| Tương thích ràng buộc `SAD §7` (không thêm hạ tầng mới) | ✅ | ❌ Vi phạm trực tiếp | ✅ |
| Tương thích năng lực đội hiện tại (`CON-05`) | ✅ | 🔴 Chưa xác nhận có kinh nghiệm | ✅ Tái dùng hạ tầng JWT sẵn có |
| Chi phí đảo ngược | Thấp (nhưng để lại rủi ro) | Cao | Trung bình |
## 3. Quyết định
> **Chọn PA-3 — Service JWT ngắn hạn ký bởi Identity & Access, giữ nguyên security group làm lớp
> phòng thủ đầu.**
**Vì sao:** Đây là phương án duy nhất vừa nâng được một lớp phòng thủ thật sự (giảm thiểu
`THR-09`/`THR-13`) vừa **không vi phạm** ràng buộc đã ghi ở `SAD §7` v1.2 ("không tự thêm hạ tầng
mới ngoài phạm vi") và không đòi hỏi năng lực vận hành mesh mà đội chưa xác nhận có (`CON-05`).
**Phạm vi áp dụng:** Mọi lời gọi cross-network nội bộ giữa các nhóm triển khai — hiện tại là TB3
(`IF-021`/`IF-022`) và TB4 (`IF-006`); áp dụng cho mọi `IF` cross-network tương lai theo cùng
chuẩn (thiết lập tiền lệ).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| **Có đại diện Security thật ký** — đây là quyết định bảo mật, Security có quyền phủ quyết | Security | 🔴 **Chặn cứng — chưa có người** (`OQ-007`, mở từ GĐ1) |
| Tech Lead xác nhận hướng PA-3 (không đổi sang PA-2 mTLS) | Tech Lead | Mở (`OQ-050` chưa đóng) |
| Đo lại latency overhead của bước xin/verify token, xác nhận không ăn quá nhiều vào ngân sách `QAS-002`/`QAS-010` | Tech Lead + QA | Chưa chạy — GĐ3 |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Chỉ dựa network | Không giảm thiểu `THR-09`/`THR-13` (Spoofing mức 🔴) — để lại rủi ro bảo mật đã biết mà không có biện pháp bổ sung | `ASR-002`, `THR-13` | Không xét lại — đây là hiện trạng đã bị đánh giá không đủ |
| PA-2 — mTLS/App Mesh | Vi phạm trực tiếp ràng buộc `SAD §7` v1.2 ("không tự thêm hạ tầng mới ngoài phạm vi"); đội chưa xác nhận có kinh nghiệm vận hành mesh (`CON-05`) | `SAD §7`, `CON-05` | Xét lại nếu tổ chức cần zero-trust đầy đủ hơn (VD mở rộng nhiều team/service hơn đáng kể) **và** có `ADR` riêng chấp nhận thêm hạ tầng mesh, **và** đội xác nhận đủ năng lực vận hành |
## 5. Hệ quả
**Hệ quả tích cực**
- Nâng một lớp phòng thủ thật sự cho TB3/TB4 (giảm thiểu `THR-09`/`THR-13`), thay vì chỉ dựa
network reachability
- Không cần hạ tầng mới — tái dùng hạ tầng ký/verify JWT sẵn có của Identity & Access
- Thiết lập một chuẩn chung cho mọi `IF` cross-network tương lai
**Hệ quả tiêu cực phải sống chung**
- Identity & Access cần thêm luồng cấp phát service token (endpoint/logic mới, dù nhỏ)
- Thêm 1 round-trip mạng (xin token) có thể ăn vào ngân sách latency vốn đã eo hẹp
(`QAS-002`/`QAS-010`) — cần đo lại trước khi `Accepted`
- Vẫn phụ thuộc network layer làm lớp phòng thủ đầu — không phải zero-trust đầy đủ như mTLS
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi sang mTLS/App Mesh sau này | Cần viết `ADR` mới chấp nhận thêm hạ tầng, triển khai mesh, đào tạo đội vận hành — nhiều tuần |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Identity & Access thiết kế endpoint/logic cấp service token | Dev (Identity & Access) | Trước khi Dev thi công TB3/TB4 | Thiết kế chi tiết thuộc GĐ3 |
| Đo lại latency overhead trên đường găng checkout | SA/QA (GĐ3) | Trước go-live | `FAIL`/`FIT` GĐ3 |
| Viết `FIT-31` thật (service-authn test) | QA/Dev (GĐ3) | GĐ3 | `03-enablement/FIT_e-commerce.md` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Gọi `IF-006`/`IF-021`/`IF-022` không có service JWT hợp lệ (thiếu, hết hạn, hoặc sai chữ ký) bị từ chối | Integration test | CI + staging | `FIT-31` *(ứng viên, đã đề xuất ở `SEC §11`)* |
| Đo p95 latency đường găng checkout với bước xin/verify token, xác nhận vẫn ≤ ngân sách `QAS-002` | Load test (k6) | Staging | Ứng viên bổ sung — cùng nhóm `FIT-23/24/25` của `ADR-013` |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Latency overhead đo được ăn quá nhiều vào margin `QAS-002` (margin còn lại theo `ADR-013` ~1.280–1.880ms) | Overhead > 100ms p95 (đề xuất, chưa xác nhận) | Tech Lead |
| Tổ chức mở rộng nhiều team/service hơn đáng kể, cần zero-trust đầy đủ | Số service cross-network > gấp đôi hiện tại (2 ranh giới TB3/TB4) | Tech Lead + Security |
## 8. Tham chiếu
- POC: Không bắt buộc (radar 5/10, dưới ngưỡng 8) — nhưng khuyến nghị đo lại latency trước khi
`Accepted`
- Bài đo: `FIT-31` (ứng viên GĐ3)
- Tài liệu ngoài: —
- Thảo luận: `SEC_e-commerce_v1.0.md §2.4` (phát hiện + đề xuất), `THR-09`/`THR-13`,
`OQ-050`/`DEC-28` (chọn hướng TẠM)

View File

@@ -0,0 +1,168 @@
# ADR-016 — Chiến lược triển khai Blue-Green qua AWS CodeDeploy cho 3 dịch vụ ECS
*Tên file: `adr/ADR-016_blue-green-deploy-aws-codedeploy.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Ops/SRE (hạ tầng/HA/DR/chi phí vận hành — `decision-radar.md §5`; PO phủ quyết nếu vượt ngân sách) — chưa ký thật |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **6/10** *(chấm theo `decision-radar.md §2`, kế thừa nguyên ước lượng đã ghi ở `INF_e-commerce_v1.0.md §7.2`: Chi phí đảo ngược=1 (đổi chiến lược deploy sau khi đã cấu hình CodeDeploy cho 3 service tốn vài tuần, không phải migrate dữ liệu) · Bán kính ảnh hưởng=2 (toàn bộ 3 đơn vị triển khai — Nhóm Giao dịch, Nhóm Hỗ trợ, Payment) · Chạm `QAS` Must=2 (trực tiếp `QAS-007`, ngân sách lỗi 43 phút/tháng) · Ràng buộc dài hạn=1 (ràng buộc trong phạm vi chiến lược release hiện tại, đổi được qua `ADR` mới) · Tranh cãi=0 → **6 ⇒ `ADR` bắt buộc**, dưới ngưỡng 8, không bắt buộc POC)* |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `QAS-007` · `ASR-011` · `OQ-020` · `OQ-056` · `ADR-010` · `ADR-012` · `INF_e-commerce_v1.0.md §6.4, §7` · `DEC-30` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-016` và đổi trạng thái bản cũ thành `Superseded by`. Không sửa nội dung ADR
> đã `Accepted`, không xoá ADR đã `Rejected`.
---
## 0. Ghi chú Preflight & ngoại lệ gate
Viết cùng lượt với `ADR-014`/`ADR-015`/`ADR-017` dưới ngoại lệ gate ghi ở `DEC-31` (tiếp nối
`DEC-01…30`) — xem `ADR-014 §0` cho bối cảnh đầy đủ. Riêng `ADR-016`: hiện thực hoá đề xuất đã ghi
ở `INF_e-commerce_v1.0.md §7.2` (duyệt từng phần, `DEC-30`).
---
## 1. Bối cảnh
`QAS-007` (error budget) đặt ngân sách lỗi **43 phút/tháng** cho Catalog/Checkout/Payment/Identity
(99,9%/tháng), loại trừ bảo trì có báo trước ≤2 giờ/tháng (`OQ-020`, TẠM — chưa PO xác nhận số
cuối). `INF §6.4` ghi rõ ràng buộc: chiến lược deploy **không được tự tiêu tốn ngân sách lỗi này**
— chọn sai chiến lược release (VD rolling update có cửa sổ mất traffic ngắn mỗi lần) tích luỹ
qua nhiều lần release/tháng có thể ăn hết 43 phút mà không liên quan gì tới sự cố thật.
`ADR-012` đã chốt 3 đơn vị triển khai độc lập: Nhóm Giao dịch, Nhóm Hỗ trợ, Payment (ECS Fargate).
Mỗi service cần một chiến lược release nhất quán, không phải mỗi module một kiểu.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `QAS-007` | Ngân sách lỗi 43 phút/tháng, loại trừ bảo trì ≤2h/tháng (`OQ-020`, TẠM) |
| `ASR-011` | Observability & alerting — cần phát hiện lỗi sau deploy nhanh để rollback kịp thời |
| `CON-08` | Năng lực đội vận hành chưa xác nhận đầy đủ — chọn chiến lược không quá phức tạp để đội vận hành được |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| AWS CodeDeploy hỗ trợ blue-green cho ECS như một tính năng managed sẵn có | 🟢 Đã kiểm chứng — theo tài liệu AWS công khai | `INF §7.2` |
| Đội chưa từng vận hành CodeDeploy blue-green trong production | 🔴 Giả định — chưa xác nhận | `CON-05`/`CON-08`, `OQ-008` |
| Rollback bằng chuyển traffic ALB (green→blue) là tức thời (giây), không cần redeploy | 🟢 Đã kiểm chứng — tính năng CodeDeploy | `INF §7.2` |
## 2. Phương án đã cân nhắc
### PA-1 — Rolling update
| | |
|---|---|
| **Mô tả** | Thay thế dần từng task cũ bằng task mới, giữ một phần traffic route tới task đang tắt trong thời gian ngắn |
| **Ưu** | Đơn giản, là chế độ mặc định của ECS, không cần cấu hình CodeDeploy thêm |
| **Nhược** | Có cửa sổ ngắn (vài giây) mỗi lần deploy mà request có thể chạm task đang tắt (draining) — tích luỹ nhiều lần release/tháng có nguy cơ ăn vào ngân sách lỗi 43 phút/tháng (`QAS-007`); rollback chậm hơn (phải redeploy version cũ, không phải chuyển traffic tức thời) |
| **Chi phí đảo ngược** | Thấp — đổi sang blue-green sau vẫn khả thi, không tốn nhiều |
### PA-2 — Blue-green qua AWS CodeDeploy *(chọn)*
| | |
|---|---|
| **Mô tả** | Deploy phiên bản mới sang một target group "green" song song với "blue" đang chạy; sau khi health check pass, chuyển toàn bộ traffic ALB sang "green" dứt điểm; giữ "blue" một thời gian để rollback tức thời nếu phát hiện lỗi |
| **Ưu** | Tương thích ngân sách lỗi chặt (`QAS-007`) — 0 downtime theo thiết kế nếu health check đúng; rollback tức thời (chuyển traffic lại, không cần redeploy) |
| **Nhược** | Cần double capacity tạm thời trong cửa sổ chuyển đổi (cả blue lẫn green cùng chạy) — tăng chi phí nhỏ trong thời gian ngắn; đội cần học vận hành CodeDeploy (chưa có kinh nghiệm, `CON-05`/`CON-08`) |
| **Chi phí đảo ngược** | Trung bình — đổi sang canary sau này là bổ sung, không phải viết lại từ đầu |
### PA-3 — Canary (progressive traffic shifting)
| | |
|---|---|
| **Mô tả** | Chuyển traffic dần theo tỷ lệ % (VD 10% → 50% → 100%) sang phiên bản mới, theo dõi metrics ở mỗi bước |
| **Ưu** | Phát hiện lỗi sớm hơn với blast radius nhỏ hơn (chỉ % nhỏ traffic chạm phiên bản lỗi) |
| **Nhược** | Thêm độ phức tạp giám sát (theo dõi metrics theo từng bước %, quyết định tự động/thủ công tiếp tục hay rollback ở mỗi bước) mà đội chưa có kinh nghiệm (`CON-05`); lợi ích thêm (phát hiện sớm hơn blue-green) chưa cần thiết ở quy mô tải hiện tại (kịch bản A/B của `TCO`) |
| **Chi phí đảo ngược** | Trung bình-cao — cấu hình canary phức tạp hơn, tháo dỡ cũng tốn công hơn |
### Bảng so sánh
| Tiêu chí | PA-1 | PA-2 | PA-3 |
|---|---|---|---|
| Downtime mỗi lần deploy | Cửa sổ ngắn (vài giây) | 0 theo thiết kế | 0 theo thiết kế |
| Tương thích `QAS-007` (ngân sách lỗi chặt) | 🔴 Rủi ro tích luỹ | ✅ | ✅ |
| Tốc độ rollback | Chậm (redeploy) | Tức thời (chuyển traffic) | Tức thời từng bước |
| Độ phức tạp vận hành so với năng lực đội hiện tại (`CON-05`) | Thấp | Trung bình | Cao |
## 3. Quyết định
> **Chọn PA-2 — Blue-green qua AWS CodeDeploy cho cả 3 dịch vụ ECS (Nhóm Giao dịch, Nhóm Hỗ trợ,
> Payment).**
**Vì sao:** Đây là phương án cân bằng tốt nhất giữa việc bảo vệ ngân sách lỗi chặt (`QAS-007`) và
độ phức tạp vận hành phù hợp với năng lực đội hiện tại (`CON-05`) — mạnh hơn rolling (không có
cửa sổ mất traffic), đơn giản hơn canary (không cần giám sát theo từng bước %).
**Phạm vi áp dụng:** Cả 3 dịch vụ ECS triển khai (Nhóm Giao dịch, Nhóm Hỗ trợ, Payment) — thống
nhất một chiến lược, không mỗi module một kiểu.
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Ops/SRE xác nhận năng lực vận hành CodeDeploy (đào tạo nếu cần) | Ops/SRE | Mở (`OQ-008` đội Ops thật chưa xác nhận) |
| Chạy thử blue-green + rollback thành công ít nhất 1 lần ở staging | Ops/SRE | Chưa chạy |
| PO xác nhận số cuối cửa sổ bảo trì loại trừ ngân sách lỗi (`OQ-020`) | PO | Mở |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Rolling update | Cửa sổ mất traffic ngắn mỗi lần deploy tích luỹ có nguy cơ ăn vào ngân sách lỗi chặt `QAS-007` (43 phút/tháng); rollback chậm hơn | `QAS-007` | Không xét lại trừ khi `QAS-007` được PO hạ mức (nới ngân sách lỗi) — quyết định nghiệp vụ, không phải kỹ thuật |
| PA-3 — Canary | Thêm độ phức tạp giám sát/vận hành mà đội chưa có kinh nghiệm (`CON-05`); lợi ích thêm (blast radius nhỏ hơn) chưa cần thiết ở quy mô tải hiện tại | `CON-05` | Xét lại khi tải tăng đáng kể (vượt kịch bản B ×2 theo `SAD §10`) hoặc đội xác nhận đủ năng lực vận hành canary |
## 5. Hệ quả
**Hệ quả tích cực**
- Bảo vệ ngân sách lỗi `QAS-007` — 0 downtime theo thiết kế nếu health check đúng
- Rollback tức thời (giây) khi phát hiện lỗi sau khi đã chuyển traffic
**Hệ quả tiêu cực phải sống chung**
- Cần double capacity tạm thời trong cửa sổ chuyển đổi (cả blue lẫn green) — tăng chi phí nhỏ,
ngắn hạn, cần xác nhận không đáng kể so với `TCO`
- Đội cần đào tạo vận hành CodeDeploy nếu chưa có kinh nghiệm (`TCO §5`)
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi sang canary sau này | Bổ sung cấu hình giám sát theo bước %, không phải viết lại toàn bộ pipeline — chi phí trung bình |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Cấu hình CodeDeploy thật cho cả 3 service | Ops/SRE (GĐ3) | Trước go-live | `03-enablement/AGD_e-commerce.md` (chưa tồn tại) |
| Viết runbook rollback chi tiết | Ops/SRE (GĐ3) | Trước go-live | `03-enablement/AGD_e-commerce.md` |
| Chạy thử blue-green + rollback ở staging | Ops/SRE | Trước AG2 | `INF §11 FIT-37` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Blue-green deploy tự động rollback khi health check fail sau khi chuyển traffic | CodeDeploy hook test | Staging, trước mỗi major release | `FIT-37` *(đã đề xuất ở `INF §11`)* |
| Đo downtime thực tế trong 1 lần deploy blue-green, xác nhận = 0 (hoặc dưới ngưỡng nhỏ chấp nhận được) | Load test liên tục trong lúc deploy | Staging | Ứng viên bổ sung, cùng nhóm `FIT-37` |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Tần suất release tăng đáng kể, cần giảm blast radius hơn nữa | Release > vài lần/tuần với rủi ro cao (đề xuất, chưa xác nhận) | Tech Lead |
| Chi phí double-capacity trong cửa sổ deploy vượt ngân sách đáng kể | Vượt >5% chi phí compute hàng tháng (đề xuất, chưa xác nhận) | Ops/PO |
## 8. Tham chiếu
- POC: Không bắt buộc (radar 6/10, dưới ngưỡng 8)
- Bài đo: `FIT-37` (ứng viên GĐ3, đã đề xuất ở `INF §11`)
- Tài liệu ngoài: —
- Thảo luận: `INF_e-commerce_v1.0.md §6.4, §7.2` (phát hiện + đề xuất), `DEC-30` (duyệt từng phần)

View File

@@ -0,0 +1,167 @@
# ADR-017 — Công cụ Infrastructure as Code — Terraform
*Tên file: `adr/ADR-017_iac-terraform.md`*
| | |
|---|---|
| **Status** | `Proposed` |
| **Date** | 2026-09-15 |
| **Người quyết** | Tech Lead + Ops/SRE (hạ tầng — `decision-radar.md §5`) — chưa ký thật |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **7/10** *(chấm theo `decision-radar.md §2`, kế thừa nguyên ước lượng đã ghi ở `INF_e-commerce_v1.0.md §11`: Chi phí đảo ngược=2 (>4 tuần — đổi công cụ sau khi đã có code Terraform thật cho toàn bộ hạ tầng tốn nhiều tuần viết lại + rủi ro thao tác chuyển đổi state) · Bán kính ảnh hưởng=2 (toàn bộ tài nguyên AWS của dự án) · Chạm `QAS` Must=1 (gián tiếp — nền tảng hiện thực hoá mọi `QAS` hạ tầng, không phải chính nó là một `QAS`) · Ràng buộc dài hạn=2 (chuẩn IaC nội bộ ≥1 năm) · Tranh cãi=0 → **7 ⇒ `ADR` bắt buộc**, dưới ngưỡng 8, không bắt buộc POC)* |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `CON-05` · `INF_e-commerce_v1.0.md §11` · `OQ-060` · `DEC-30` |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-017` và đổi trạng thái bản cũ thành `Superseded by`. Không sửa nội dung ADR
> đã `Accepted`, không xoá ADR đã `Rejected`.
---
## 0. Ghi chú Preflight & ngoại lệ gate
Viết cùng lượt với `ADR-014`/`ADR-015`/`ADR-016` dưới ngoại lệ gate ghi ở `DEC-31` (tiếp nối
`DEC-01…30`) — xem `ADR-014 §0` cho bối cảnh đầy đủ. Riêng `ADR-017`: hiện thực hoá đề xuất đã ghi
ở `INF_e-commerce_v1.0.md §11` (duyệt từng phần, `DEC-30`).
---
## 1. Bối cảnh
Trước khi `sa-3-enablement` (GĐ3) viết code hạ tầng thật, cần chốt công cụ Infrastructure as Code
(IaC) chính cho toàn dự án — tránh tình trạng mỗi thành phần hạ tầng dùng một công cụ khác nhau,
không ai review/audit được nhất quán. `INF_e-commerce_v1.0.md §11` đã đề xuất Terraform và đánh
dấu đây là quyết định thiết lập tiền lệ (radar ước lượng ~7, đạt ngưỡng `ADR` bắt buộc), yêu cầu
SA viết `ADR` chính thức ở hoạt động `adr`.
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `CON-05` | Năng lực đội (kỹ năng lập trình cho hạ tầng — TypeScript/Python cho CDK, hay chỉ cần HCL của Terraform) chưa xác nhận rõ |
| `D9` (`design-rules.md`) | Mọi con số hạ tầng phải quy ra tiền, có nguồn, và cấu hình phải tái lập được — công cụ IaC là nền tảng để đạt điều này |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Terraform có cộng đồng module lớn cho ECS/RDS/ElastiCache (AWS) | 🟢 Đã kiểm chứng — thực tế công khai của hệ sinh thái Terraform | `INF §11` |
| Đội có đủ kỹ năng TypeScript/Python để dùng AWS CDK hiệu quả | 🔴 Giả định chưa xác nhận | `CON-05`, `OQ-060` |
| Đội có đủ kỹ năng HCL (ngôn ngữ cấu hình của Terraform) hoặc học được nhanh hơn một ngôn ngữ lập trình đầy đủ | 🔴 Giả định — Terraform HCL thường được coi là dễ tiếp cận hơn với đội chưa quen lập trình hạ tầng, nhưng chưa xác nhận với đội thật | `OQ-060` |
## 2. Phương án đã cân nhắc
### PA-1 — Terraform *(chọn)*
| | |
|---|---|
| **Mô tả** | HashiCorp Terraform, dùng HCL để định nghĩa hạ tầng, quản lý state qua backend (đề xuất S3 + DynamoDB lock) |
| **Ưu** | Đa cloud-agnostic hơn CDK (dù hiện chỉ dùng AWS — giữ tuỳ chọn mở cho tương lai); cộng đồng module lớn cho ECS/RDS/ElastiCache; không khoá vào một ngôn ngữ lập trình cụ thể (HCL là ngôn ngữ khai báo riêng, không cần biết TypeScript/Python) |
| **Nhược** | HCL là một ngôn ngữ riêng cần học, dù thường được coi là dễ tiếp cận hơn ngôn ngữ lập trình đầy đủ; cần thiết lập backend state (S3 + DynamoDB lock) — thêm một phần hạ tầng nhỏ |
| **Chi phí đảo ngược** | Cao (>4 tuần) nếu đổi công cụ sau khi đã có code Terraform thật cho toàn bộ hạ tầng — viết lại + rủi ro thao tác trong lúc chuyển đổi state |
### PA-2 — AWS CDK
| | |
|---|---|
| **Mô tả** | Định nghĩa hạ tầng bằng ngôn ngữ lập trình đầy đủ (TypeScript/Python), compile ra CloudFormation |
| **Ưu** | Tích hợp sâu với AWS, cho phép dùng logic lập trình (vòng lặp, hàm) trực tiếp khi định nghĩa hạ tầng |
| **Nhược** | Cần kỹ năng TypeScript/Python cho hạ tầng — `CON-05` chưa xác nhận đội có; khoá vào AWS (dù hiện tại dự án chỉ dùng AWS, việc khoá vào một hãng làm giảm tuỳ chọn multi-cloud tương lai nếu cần) |
| **Chi phí đảo ngược** | Cao — tương tự PA-1, đổi sau khi đã có code thật tốn nhiều tuần |
### PA-3 — AWS CloudFormation (native)
| | |
|---|---|
| **Mô tả** | Dùng trực tiếp CloudFormation (JSON/YAML), không qua công cụ trung gian |
| **Ưu** | Không cần công cụ bên thứ ba, tích hợp gốc với AWS Console/CLI |
| **Nhược** | Cú pháp JSON/YAML kém module hoá hơn Terraform/CDK; cộng đồng module tái sử dụng nhỏ hơn; quản lý state kém linh hoạt hơn (CloudFormation tự quản lý state, không có backend tách rời như Terraform S3+DynamoDB, khó tách theo môi trường/nhóm tài nguyên để giảm blast radius) |
| **Chi phí đảo ngược** | Trung bình-cao — viết lại theo cấu trúc module hoá tốt hơn sau này |
### Bảng so sánh
| Tiêu chí | PA-1 (Terraform) | PA-2 (CDK) | PA-3 (CloudFormation) |
|---|---|---|---|
| Phù hợp năng lực đội hiện tại (`CON-05`, chưa xác nhận kỹ năng lập trình) | 🟡 Cần học HCL (thường dễ hơn) | 🔴 Cần TypeScript/Python | 🟡 Cần học JSON/YAML CloudFormation |
| Đa cloud-agnostic (giữ tuỳ chọn tương lai) | ✅ | ❌ Khoá vào AWS | ❌ Khoá vào AWS |
| Cộng đồng module tái sử dụng (ECS/RDS/ElastiCache) | ✅ Lớn | 🟡 Trung bình | 🔴 Nhỏ hơn |
| Tách state theo môi trường/nhóm tài nguyên (giảm blast radius) | ✅ Linh hoạt (backend riêng) | 🟡 Qua CDK stack | 🔴 Kém linh hoạt hơn |
## 3. Quyết định
> **Chọn PA-1 — Terraform làm công cụ Infrastructure as Code chính cho toàn dự án.**
**Vì sao:** Cân bằng tốt nhất giữa năng lực đội chưa xác nhận rõ (`CON-05`, không đòi hỏi kỹ năng
lập trình đầy đủ như CDK), hệ sinh thái module tái sử dụng lớn cho đúng các dịch vụ AWS đã chọn
(ECS/RDS/ElastiCache), và khả năng tách state để giảm blast radius (yêu cầu (h) của `INF §11`).
**Phạm vi áp dụng:** Toàn bộ hạ tầng AWS của dự án — network, data, compute — trong mọi môi trường
(dev/stg/prod).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| Tech Lead xác nhận công cụ IaC (Terraform) cho P2 | Tech Lead | Mở (`OQ-060` chưa đóng) |
| Ops/SRE xác nhận năng lực vận hành Terraform (đào tạo nếu cần) | Ops/SRE | Mở |
| Thiết lập backend state (S3 + DynamoDB lock) thành công ở dev/stg | Ops/SRE | Chưa chạy — GĐ3 |
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-2 — AWS CDK | Cần kỹ năng TypeScript/Python cho hạ tầng mà `CON-05` chưa xác nhận đội có; khoá vào AWS làm giảm tuỳ chọn multi-cloud tương lai dù hiện chưa cần | `CON-05` | Xét lại nếu đội xác nhận có kỹ năng lập trình mạnh (TypeScript/Python) và muốn tích hợp logic lập trình phức tạp trực tiếp vào định nghĩa hạ tầng |
| PA-3 — CloudFormation | Cộng đồng module nhỏ hơn, kém tái sử dụng, quản lý state kém linh hoạt hơn so với Terraform (backend S3+DynamoDB tách rời) | `INF §11` (yêu cầu tách state theo môi trường/nhóm) | Xét lại nếu tổ chức có chính sách bắt buộc chỉ dùng công cụ AWS gốc (không có ở dự án này hiện tại) |
## 5. Hệ quả
**Hệ quả tích cực**
- Module hoá tốt, tách state theo môi trường/nhóm tài nguyên — giảm blast radius khi
`terraform apply`
- Drift detection dễ tích hợp CI (`terraform plan` định kỳ, `FIT-34`)
- Không khoá vào một ngôn ngữ lập trình cụ thể — giảm rào cản với đội chưa xác nhận kỹ năng
TypeScript/Python
**Hệ quả tiêu cực phải sống chung**
- Cần đào tạo Terraform/HCL cho đội nếu chưa quen (`TCO §5`)
- Cần thiết lập và vận hành backend state riêng (S3 + DynamoDB lock) — thêm một phần hạ tầng nhỏ
cần quản lý (quyền truy cập, backup state)
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Đổi công cụ IaC sau khi đã có code Terraform thật cho toàn bộ hạ tầng | Viết lại toàn bộ + rủi ro thao tác trong lúc migrate state — nhiều tuần |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Viết Terraform module thật cho network/data/compute | Ops/SRE + Dev (GĐ3) | Trước go-live | `03-enablement/AGD_e-commerce.md` (chưa tồn tại) |
| Thiết lập backend state (S3 + DynamoDB lock) | Ops/SRE | Trước khi viết module thật | GĐ3 |
| Viết `FIT-34`/`FIT-35`/`FIT-36` thật | Ops/SRE (GĐ3) | GĐ3 | `03-enablement/FIT_e-commerce.md` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| Drift detection — hạ tầng thật khớp Terraform state | `terraform plan` định kỳ / driftctl | CI, hàng ngày | `FIT-34` *(đã đề xuất ở `INF §11`)* |
| Tag policy — mọi tài nguyên có đủ 4 tag bắt buộc | AWS Config rule / OPA | CI + AWS Config liên tục | `FIT-35` *(đã đề xuất ở `INF §11`)* |
| Đúng cấu trúc hạ tầng (2 ECS service GD/HT + 1 Payment, đúng SG cô lập) | Terraform plan review + AWS Config | CI/CD | `FIT-36` *(đã đề xuất ở `INF §11`)* |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| Đội xác nhận có kỹ năng TypeScript/Python mạnh và muốn logic lập trình phức tạp trong định nghĩa hạ tầng | Ý kiến chính thức từ Tech Lead sau khi đội đã dùng Terraform một thời gian | Tech Lead |
| Nhu cầu multi-cloud thật sự phát sinh | Có quyết định kinh doanh mở rộng ngoài AWS | PO + Tech Lead |
## 8. Tham chiếu
- POC: Không bắt buộc (radar 7/10, dưới ngưỡng 8)
- Bài đo: `FIT-34`/`FIT-35`/`FIT-36` (ứng viên GĐ3, đã đề xuất ở `INF §11`)
- Tài liệu ngoài: —
- Thảo luận: `INF_e-commerce_v1.0.md §11` (phát hiện + đề xuất), `OQ-060`, `DEC-30`