Files
sys-analysis-design/sa-output/e-commerce/02-architecture/adr/ADR-014_transactional-outbox-publish-su-kien.md
Canhchimlac 343ad8bbbc save
2026-09-15 16:02:30 +07:00

18 KiB
Raw Blame History

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)