# 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ý.