Skip to content

Architecture Decision Records (ADRs) ​

Các bản ghi quyết định kiến trúc (ADR) được viết theo format Vấn đề → Lựa chọn → Lý do → Đánh đổi. ADR-002, ADR-006 → ADR-013 là quyết định đã áp dụng trong Core Banking (Giai đoạn 1), kèm vấn đề kỹ thuật thực tế gặp phải khi triển khai. ADR-001, ADR-004, ADR-005 là quyết định dự kiến cho Payment Gateway (Giai đoạn 2), sẽ cập nhật khi code. Tiến độ chi tiết xem tại Devlog.


Ma trận Quyết định Kiến trúc ​

Mã ADRVấn đề kỹ thuậtQuyết định lựa chọnĐánh đổi chấp nhận
ADR-001Quản lý giao dịch phân tán liên ngân hàngSaga Orchestration (thay vì Choreography)Orchestrator có thể thành điểm nghẽn nếu không thiết kế stateless
ADR-002Mô hình ghi nhận biến động số dư tài khoảnSổ cái kế toán kép, chỉ ghi thêm (append-only)Dữ liệu ledger_entries tăng liên tục theo thời gian
ADR-003Xung đột đồng thời trên tài khoản nóng (hot account)Pessimistic Lock + khóa theo thứ tự cố địnhĐộ trễ tăng nhẹ trên từng tài khoản đơn lẻ
ADR-004Chống xử lý trùng lặp khi message bị gửi lạiIdempotency-Key kiểm tra tại lớp APICần chính sách dọn dẹp/hết hạn cho bảng idempotency key
ADR-005Xử lý đối soát & quyết toán cuối ngày (EOD)Spring Batch Chunk-Oriented StepPhức tạp hơn Tasklet đơn giản, cần tinh chỉnh chunk size
ADR-006Lưu trữ số dư tài khoản (balance)Không lưu cột balance, tính động từ SUM(CREDIT) - SUM(DEBIT) trên ledgerTốn chi phí truy vấn SUM() mỗi lần gọi, cần cân nhắc snapshot khi hệ thống lớn hơn
ADR-007Race condition check-then-act khi rút tiền đồng thờiPessimistic Locking (SELECT ... FOR UPDATE) thay vì Optimistic LockingGiảm throughput trên cùng 1 account, cần cân nhắc lock timeout khi mở rộng
ADR-008Invariant Debit=Credit nằm ngoài entity, ở tầng serviceTransaction Aggregate Root — LedgerEntry chỉ tạo được qua Transaction.record()Thêm bảng transactions + migration backfill; entity dùng Persistable<UUID> do ID application-assigned
ADR-009Rủi ro Dual Write khi ghi DB + gửi Kafka trực tiếp cùng flowOutbox Pattern — lưu event vào outbox_events cùng transaction DB, publish qua tiến trình @Scheduled riêngAt-least-once delivery, bắt buộc Idempotent Consumer (processed_events) ở phía nhận
ADR-010getBalance() tính lại SUM() toàn bộ ledger mỗi lần gọi, không hiệu quảRedis cache-aside cho getBalance(), evict qua @TransactionalEventListener(AFTER_COMMIT); withdraw() luôn tính thẳng từ DB, không qua cacheThêm độ phức tạp (event + listener) so với evict trực tiếp; TTL 3600s cho phần đọc không qua evict
ADR-011Cần minh chứng khả năng vận hành trên CSDL doanh nghiệp (Oracle) mà không phá vỡ test Postgres hiện cóDual-profile Spring — 2 bộ migration tách thư mục theo vendor, cùng 1 codebaseDuy trì 2 bộ migration song song, tăng chi phí bảo trì dài hạn
ADR-012Oracle NUMBER không giữ scale cố định lúc đọc; ISO 4217 VND=0 chữ số thập phân mâu thuẫn với domain model hiện cóMoney enforce scale=2 cố định cho mọi currency (setScale(2, RoundingMode.UNNECESSARY)), không theo ISO 4217 tuyệt đốiCần revisit nếu sau này hỗ trợ currency có native fraction digit khác 2 (VD JPY=0, BHD=3)
ADR-013Cần verify Pessimistic Locking hoạt động đúng khi nhiều instance app chạy song songDeploy app vào K8s (3 replicas), giữ data layer ở docker-compose; verify qua HTTP thật + log theo tên PodOutboxEventPublisher chưa có lock phân tán — 3 consumer PoC có thể log trùng khi nhiều Pod cùng đọc 1 event

Simulated Banking & Fintech Systems — Java & Spring Boot