Event Sourcing và CQRS trong Core Banking: Vì sao Ledger là nguồn sự thật?
Giải thích kiến trúc Event Sourcing và CQRS trong core banking, từ immutable ledger, double-entry, balance projection, idempotency đến replay, reconciliation và eventual consistency.
ShanverseUpdated 11 min read— viewsLoading views
Event-Driven Architecture
Payment Systems
Distributed Systems
Vì sao một cột balance là chưa đủ?
Giả sử tài khoản có số dư 8.000.000đ. Nếu hệ thống chỉ chạy:
thì sau đó ta chỉ biết số dư còn 7.500.000đ. Ta không thể trả lời chắc chắn:
Ai tạo biến động và qua kênh nào?
Lệnh nghiệp vụ ban đầu là gì?
Bút toán đối ứng nằm ở đâu?
Giao dịch đã bị hoàn, sửa hay hủy chưa?
Số dư tại thời điểm cuối tháng trước là bao nhiêu?
Vì sao hệ thống báo cáo và số dư khách hàng lệch nhau?
Comments
Trong tài chính, lịch sử biến động là dữ liệu nghiệp vụ, không phải log phụ. Vì thế hệ thống ghi các sự kiện và bút toán bất biến, rồi suy ra trạng thái hiện tại.
Ba khái niệm thường bị trộn lẫn
Ledger — sổ cái
Ledger lưu các bút toán tài chính. Mỗi giao dịch double-entry tạo ít nhất hai entry cân bằng: một bên debit và một bên credit. Tổng giá trị của các entry trong cùng transaction phải bằng 0 theo quy ước dấu.
Event Sourcing
Event Sourcing lưu chuỗi sự kiện nghiệp vụ theo thứ tự, ví dụ:
TransferRequested
FundsReserved
TransferPosted
TransferRejected
TransferReversed
Trạng thái aggregate được dựng bằng cách fold các event. Event diễn tả điều đã xảy ra, không chỉ một giá trị mới.
CQRS
CQRS tách mô hình ghi lệnh khỏi mô hình đọc:
Command side: kiểm tra invariant và append event/ledger entries.
Query side: phục vụ số dư, lịch sử giao dịch, sao kê và dashboard từ projection tối ưu cho đọc.
CQRS không bắt buộc Event Sourcing; Event Sourcing cũng không bắt buộc CQRS. Chúng thường đi cùng nhau vì event stream rất phù hợp để xây nhiều read model.
Kiến trúc tổng thể
Mermaid
Loading diagram…
Command side phải là nơi duy nhất được phép quyết định thay đổi tiền. Query side không được biến một read model thành nguồn sự thật chỉ vì nó đọc nhanh.
Mô hình dữ liệu tham khảo
Event store
sequence_no cung cấp optimistic concurrency: command chỉ append event tiếp theo nếu version aggregate chưa thay đổi.
Journal và entries
Tiền nên dùng integer minor units hoặc decimal chính xác, không dùng floating point. Quy tắc cân bằng phải được xác minh trong transaction hoặc qua posting engine không cho phép ghi journal chưa cân.
Projection số dư
Bảng này có thể được UPDATE. Nó là cache/materialized state có version, có thể xóa và replay từ ledger/event store. Tùy yêu cầu nhất quán, projection có thể được cập nhật cùng transaction với journal hoặc bất đồng bộ qua event bus.
Một command chuyển tiền đi qua hệ thống như thế nào?
API nhận TransferMoney cùng idempotency_key.
Xác thực danh tính, quyền, hạn mức, currency và trạng thái account.
Load aggregate hoặc trạng thái cần thiết tại version hiện tại.
Kiểm tra available balance và các hold đang hiệu lực.
Sinh event nghiệp vụ cùng journal transaction cân bằng.
Trong một ACID transaction, claim idempotency key, append event, ghi journal entries và outbox.
Commit bền vững rồi mới trả receipt.
Projector cập nhật balance, statement, notification và reporting.
Không gọi dịch vụ mạng khi đang giữ database transaction. Tích hợp bên ngoài đi qua state machine, outbox/inbox và retry idempotent.
Số dư không chỉ có một loại
Một hệ thống core banking thường phân biệt:
Ledger balance: tổng các bút toán đã post.
Available balance: phần có thể chi sau khi trừ hold/reservation và cộng hạn mức phù hợp.
Pending balance: giao dịch đã nhận nhưng chưa post cuối cùng.
Value-dated balance: số dư theo ngày hiệu lực nghiệp vụ, có thể khác ngày ghi nhận.
Việc giữ ledger cho phép tái tạo từng khái niệm theo quy tắc và thời điểm, thay vì ép tất cả vào một cột mơ hồ.
Vì sao không sửa event hoặc ledger entry cũ?
Khi giao dịch sai, hệ thống không nên thay đổi quá khứ để giả vờ lỗi chưa từng xảy ra. Thay vào đó:
Ghi TransferReversalRequested.
Tạo journal transaction đảo với các entry đối ứng.
Liên kết reversal với transaction gốc.
Cập nhật projection bằng cả giao dịch gốc và giao dịch đảo.
Lịch sử lúc này cho thấy điều gì đã xảy ra, ai sửa, sửa khi nào và tác động ròng bằng bao nhiêu. Nếu dữ liệu chứa thông tin cá nhân cần xóa theo chính sách, nên tách PII khỏi financial fact, dùng tokenization/crypto-shredding và quy trình được pháp chế phê duyệt—không âm thầm phá audit trail.
Consistency: khách hàng có thấy số dư cũ không?
CQRS thường dẫn đến eventual consistency. Sau khi command commit, read model có thể chậm vài mili giây hoặc vài giây.
Các chiến lược xử lý:
Trả receipt cùng transaction_id, status và committed_sequence.
Query API hỗ trợ “read your writes” bằng cách chờ projection đạt sequence đó trong timeout ngắn.
Hiển thị trạng thái processing thay vì báo thất bại giả.
Cập nhật balance projection đồng transaction nếu kênh thanh toán cần strong consistency.
Không cho command side ra quyết định trên projection đang trễ; dùng aggregate/journal authoritative state.
Eventual consistency là một lựa chọn có phạm vi, không phải lý do để số dư sai vô thời hạn.
Idempotency và exactly-once thực tế
Mạng có thể timeout sau khi command đã commit. Client sẽ retry. idempotency_key phải có unique constraint trong kho bền vững và được ràng buộc với payload chuẩn hóa.
Projector và consumer cũng phải idempotent:
Chỉ cập nhật projection khi event chưa được consumer xử lý. Broker “exactly once” không tự bảo đảm side effect ngoài broker đúng một lần; database constraints và idempotent consumer vẫn cần thiết.
Ordering và optimistic concurrency
Mỗi aggregate stream có sequence tăng đơn điệu. Append event mới với expected version:
Unique constraint (account_id, sequence_no) khiến một trong hai command đồng thời thất bại. Command thua phải load lại aggregate, đánh giá lại invariant và retry có giới hạn—không chỉ đổi sequence rồi ghi mù.
Đối với invariant xuyên nhiều account, event stream riêng lẻ chưa đủ. Posting engine cần ACID transaction trên journal, lock/CAS theo thứ tự ổn định hoặc một mô hình ownership/partition phù hợp.
Replay, snapshot và event evolution
Replay là siêu năng lực nhưng cũng là rủi ro vận hành.
Event phải immutable và có event_version.
Upcaster chuyển event cũ sang representation mới khi đọc; không sửa hàng triệu event lịch sử tùy tiện.
Snapshot chỉ là tối ưu hóa và luôn có thể bỏ đi.
Projector mới phải chạy shadow, kiểm tra checksum rồi mới cut over.
Replay không được gửi lại email, thanh toán hoặc webhook; tách domain event khỏi integration side effect.
Có checkpoint, throttling và cơ chế resume để replay không làm nghẽn production.
Reconciliation: đừng chỉ tin projection
Chạy đối soát định kỳ:
Tổng debit bằng tổng credit theo transaction và currency.
Projection balance bằng tổng ledger entries tại cùng sequence/checkpoint.
Không có sequence gap hoặc duplicate event.
Không có journal posted thiếu entry.
Outbox không bị kẹt; consumer lag nằm trong SLO.
Tổng sub-ledger khớp general ledger theo quy tắc kế toán.
Sai lệch phải tạo incident và quy trình sửa bằng bút toán bù/đảo được phê duyệt.
Khi nào không nên dùng full Event Sourcing?
Event Sourcing mang lại audit, temporal query và khả năng dựng read model mới, nhưng cũng tăng độ phức tạp về schema evolution, replay, ordering, vận hành và kỹ năng đội ngũ.
Một thiết kế thực dụng có thể dùng:
Immutable double-entry ledger làm nguồn sự thật tài chính.
State machine và audit log cho workflow.
Mutable balance projection cập nhật atomically.
Outbox để phân phối event.
CQRS chỉ ở các khu vực có read/write model thực sự khác nhau.
Không cần biến mọi bảng customer profile hay cấu hình thành event-sourced aggregate.
Các anti-pattern nguy hiểm
Chỉ lưu số dư hiện tại, không có journal.
Xem Kafka topic như ledger duy nhất mà không có retention, schema và reconciliation nghiêm ngặt.
Dùng timestamp làm ordering duy nhất cho event đồng thời.
Cho phép sửa/xóa ledger entry đã post.
Dùng projection trễ để quyết định có đủ tiền hay không.
Publish event trước database commit hoặc dual-write không có outbox.
Cho replay kích hoạt lại side effect bên ngoài.
Dùng một global event stream gây hotspot cho toàn ngân hàng.
Tin rằng CQRS đồng nghĩa với microservices hoặc eventual consistency ở mọi nơi.
Metadata event nên có correlation_id, causation_id, actor/service identity, channel, trace ID, effective time và recorded time. Không nhét credential hoặc PII không cần thiết vào immutable payload.
Checklist production
Ledger/event store là authoritative; projection được đánh dấu rõ là derived.
Mỗi journal transaction cân bằng theo currency.
Tiền dùng minor units hoặc decimal chính xác.
Idempotency key và sequence có unique constraint.
Event có version, metadata và schema governance.
Command commit event, journal và outbox atomically.
Projector idempotent, có checkpoint và phát hiện gap.
Có read-your-writes hoặc trạng thái processing rõ ràng.
Sửa sai bằng reversal/compensating entry, không sửa lịch sử.
Replay an toàn, không phát side effect ngoài ý muốn.
Reconciliation tự động và có quy trình xử lý sai lệch.
Backup, restore và disaster recovery đã được diễn tập.
Kết luận
Ngân hàng không giữ lịch sử chỉ vì compliance. Ledger bất biến là cách bảo vệ bản chất của tiền: mọi biến động phải có nguyên nhân, đối ứng và khả năng đối soát. Event Sourcing lưu câu chuyện nghiệp vụ, double-entry ledger bảo đảm tính cân bằng, còn CQRS tạo các projection đọc nhanh cho số dư và sao kê.
Điểm mấu chốt không phải “cấm mọi câu UPDATE”. Điểm mấu chốt là không được biến một số dư mutable thành bằng chứng duy nhất. Projection có thể cập nhật; lịch sử tài chính đã post thì phải bất biến, có version, có bút toán đảo và có thể dựng lại.
Table of contents(24)
Codesql
1UPDATE accounts2SET balance_minor = balance_minor -5000003WHERE id = $1;
Hướng dẫn triển khai Idempotency Key cho thanh toán với PostgreSQL: unique constraint, request hash, state machine, xử lý request đồng thời, retry, timeout và chống double charge.
Hướng dẫn chống race condition và double spending trong chuyển tiền với PostgreSQL: ACID, SELECT FOR UPDATE, optimistic versioning, idempotency, deadlock, retry và kiểm thử đồng thời.
Tìm hiểu Consensus Algorithms qua sơ đồ Mermaid: cách Raft và Paxos dùng majority quorum, leader election và replicated log để đảm bảo dữ liệu nhất quán.