Case Study: Design a Digital Wallet
“Ví điện tử giống như một cuốn sổ cái bất tử — mỗi đồng tiền di chuyển đều được ghi lại dưới dạng sự kiện, không bao giờ bị xóa, không bao giờ bị sửa. A mất đúng X đồng, B nhận đúng X đồng, dù hệ thống có crash giữa chừng. Đó là sức mạnh của event sourcing.”
Tags: system-design digital-wallet event-sourcing cqrs saga exactly-once fintech alex-xu-vol2 case-study Student: Hieu Prerequisite: Tuan-02-Back-of-the-envelope · Tuan-08-Message-Queue · Tuan-11-Microservices-Pattern Lien quan: Case-Design-Payment-System · Tuan-15-Data-Security-Encryption · Tuan-14-AuthN-AuthZ-Security · Tuan-07-Database-Sharding-Replication Reference: Alex Xu, System Design Interview Volume 2 — Chapter 12: Digital Wallet
1. Context & Why — Tại sao Digital Wallet quan trọng?
1.1 Analogy: Ví điện tử MoMo / ZaloPay
Bạn hãy tưởng tượng mình đang xây hệ thống cho MoMo hoặc ZaloPay. Mỗi ngày có hàng triệu người dùng chuyển tiền cho nhau — từ việc chia tiền ăn trưa 50,000 VND đến chuyển tiền thuê nhà 5,000,000 VND. Khi người A chuyển 100,000 VND cho người B, hệ thống phải đảm bảo một cách tuyệt đối:
- A mất đúng 100,000 VND — không mất nhiều hơn, không mất ít hơn
- B nhận đúng 100,000 VND — không nhận nhiều hơn, không nhận ít hơn
- Tổng số tiền trong hệ thống không thay đổi — không bao giờ “tạo ra” hoặc “mất đi” tiền
- Không bao giờ xảy ra tình trạng A mất tiền mà B không nhận — dù server crash giữa chừng
- Không bao giờ xảy ra tình trạng B nhận tiền mà A không mất — dù network timeout
- Giao dịch chỉ được xử lý đúng một lần — dù client gửi lại request 5 lần
Đây là bài toán khó nhất trong fintech. Nó không chỉ là “trừ và cộng số dư”. Nó là bài toán về distributed consistency, fault tolerance, và exactly-once semantics trong hệ thống phân tán.
1.2 Tại sao Digital Wallet khác với Payment System?
| Khía cạnh | Payment System (Chapter 7) | Digital Wallet (Chapter 12) |
|---|---|---|
| Focus | Xử lý thanh toán qua PSP (Stripe, PayPal) | Quản lý số dư và chuyển tiền giữa các ví nội bộ |
| External dependency | Phụ thuộc card network, bank | Chủ yếu nội bộ — kiểm soát toàn bộ |
| Tính chất tiền | Tiền “chạy qua” hệ thống | Tiền “nằm trong” hệ thống |
| Consistency | PSP đảm bảo phần lớn | Ta phải tự đảm bảo — đây là thách thức lớn nhất |
| Core pattern | Idempotency + webhook + reconciliation | Event sourcing + CQRS + Saga |
| Audit | Log-based | Event log là source of truth |
| Ví dụ | Shopee xử lý thanh toán qua VNPay | MoMo chuyển tiền từ ví A sang ví B |
Key Insight: Trong payment system, bạn “nhờ” PSP (Stripe) xử lý phần khó nhất (charge card, đối phối với bank). Trong digital wallet, em phải tự xử lý phần khó nhất — đảm bảo số dư chính xác khi chuyển tiền giữa các ví. Đây là lý do chapter này tập trung vào event sourcing và distributed transaction.
1.3 Tại sao Backend Dev cần hiểu Digital Wallet?
| Lý do | Giải thích |
|---|---|
| Mọi super app đều có wallet | Grab, Gojek, MoMo, ZaloPay, WeChat Pay — wallet là core feature |
| Event sourcing là pattern phổ biến | Không chỉ fintech — banking, gaming, e-commerce đều dùng |
| CQRS là kiến trúc quan trọng | Tách read và write — pattern cần thiết cho hệ thống lớn |
| Saga pattern cho distributed transaction | Không chỉ wallet — bất kỳ hệ thống microservice nào cũng cần |
| Interview favorite | Digital wallet kiểm tra khả năng thiết kế hệ thống có consistency cao |
1.4 Quy mô thực tế
Các hệ thống digital wallet lớn trên thế giới:
| Hệ thống | Quy mô | Đặc điểm |
|---|---|---|
| Alipay | 1.3 tỷ người dùng, 100,000+ TPS peak (Singles’ Day) | Lớn nhất thế giới |
| WeChat Pay | 900 triệu người dùng | Tích hợp siêu app |
| MoMo | 35+ triệu người dùng tại Việt Nam | Ví điện tử số 1 VN |
| GrabPay | 180+ triệu người dùng Đông Nam Á | Super app wallet |
| PayPal | 430+ triệu tài khoản | Wallet + payment gateway |
Aha Moment: MoMo xử lý hàng triệu giao dịch mỗi ngày. Mỗi giao dịch là một cặp debit/credit. Nếu chỉ 0.001% giao dịch bị sai lệch (tiền mất mà không đến), với 5 triệu giao dịch/ngày = 50 giao dịch bị lỗi. 50 khách hàng gọi hotline mỗi ngày = thảm họa dịch vụ. Đây là lý do tại sao consistency phải là tuyệt đối, không phải “gần đúng”.
2. Deep Dive — Alex Xu 4-Step Framework
Step 1: Requirements — Hiểu và giới hạn bài toán
2.1.1 Clarifying Questions
| Câu hỏi | Trả lời | Ghi chú |
|---|---|---|
| Chuyển tiền giữa ai và ai? | User-to-user (P2P) | Ví A chuyển sang ví B |
| Có hỗ trợ nạp tiền / rút tiền không? | Có, nhưng focus chuyển tiền nội bộ | Nạp/rút là integration với bank |
| Quy mô? | 1 triệu transactions/day | ~12 TPS trung bình, peak 50+ TPS |
| Exactly-once bắt buộc? | Có | Không được mất hoặc tạo ra tiền |
| Multi-currency? | Không trong scope này | Đơn giản hóa |
| Số dư có thể âm? | Không | Balance >= 0 luôn luôn |
| Transaction history? | Có | User xem lịch sử giao dịch |
| Real-time balance? | Có | User thấy số dư ngay sau giao dịch |
2.1.2 Functional Requirements
| ID | Chức năng | Mô tả chi tiết |
|---|---|---|
| FR1 | Transfer money | Chuyển tiền từ wallet A sang wallet B, đảm bảo A giảm đúng X và B tăng đúng X |
| FR2 | Balance inquiry | User xem số dư hiện tại của ví — phải chính xác và real-time |
| FR3 | Transaction history | User xem danh sách các giao dịch đã thực hiện (gửi, nhận, thất bại) |
| FR4 | Idempotent transfer | Cùng một request gửi nhiều lần chỉ được xử lý 1 lần |
| FR5 | Balance validation | Không cho chuyển tiền nếu số dư không đủ |
2.1.3 Non-Functional Requirements
| Yêu cầu | Mục tiêu | Lý do |
|---|---|---|
| Correctness | Zero money loss/creation | Tiền mất = kiện tụng, tiền tạo ra = gian lận |
| Exactly-once | Mỗi transfer chỉ thực hiện 1 lần | Duplicate = mất tiền hoặc tạo tiền |
| Consistency | Strong consistency cho balance update | Số dư phải chính xác tại mọi thời điểm |
| Availability | 99.99% uptime (~52 phút downtime/năm) | Wallet không hoạt động = user không thể chi tiêu |
| Durability | Zero data loss | Mọi giao dịch phải được lưu vĩnh viễn |
| Auditability | Mọi thay đổi có audit trail | Compliance và dispute resolution |
| Latency | P99 < 500ms cho transfer | User experience |
| Throughput | 1M transactions/day, peak 50 TPS | Scale cho việc dùng hàng ngày |
Trade-off quan trọng: Đây là hệ thống CP (Consistency > Availability theo CAP theorem). Khi phải chọn giữa “cho phép giao dịch sai” và “từ chối giao dịch”, ta luôn chọn từ chối. Tham chiếu Tuan-07-Database-Sharding-Replication để hiểu CP vs AP.
Step 2: High-Level Design — Kiến trúc tổng quan
2.2.1 System Components
| Component | Vai trò | Analogy |
|---|---|---|
| Wallet Service | Quản lý ví, kiểm tra số dư, thực hiện chuyển tiền | Thủ ngân ngân hàng |
| Transaction Service | Điều phối toàn bộ flow chuyển tiền, đảm bảo atomicity | Giám đốc chi nhánh |
| Ledger Service | Ghi nhận mọi giao dịch theo chuẩn double-entry | Sổ cái kế toán |
| Audit Service | Kiểm tra tính nhất quán, đối soát, phát hiện bất thường | Kiểm toán viên |
| Event Store | Lưu trữ mọi sự kiện (source of truth) | Cuốn nhật ký bất tử |
| Balance Cache | Cache số dư để đọc nhanh | Bảng tin ở cửa ngân hàng |
2.2.2 High-Level Architecture
flowchart TB subgraph "Client Layer" USER_A["User A<br/>Mobile App"] USER_B["User B<br/>Mobile App"] end subgraph "API Gateway" GW["API Gateway<br/>Auth, Rate Limit,<br/>Idempotency Check"] end subgraph "Core Services" TXN["Transaction Service<br/>Transfer Orchestrator"] WALLET["Wallet Service<br/>Balance Management"] LEDGER["Ledger Service<br/>Double-entry Bookkeeping"] AUDIT["Audit Service<br/>Reconciliation & Compliance"] end subgraph "Event Infrastructure" ES["Event Store<br/>Append-only Log"] KAFKA["Message Broker<br/>Kafka"] end subgraph "Data Stores" WALLETDB[("Wallet DB<br/>Balance State")] LEDGERDB[("Ledger DB<br/>Immutable Records")] CACHE[("Redis<br/>Balance Cache")] HISTORYDB[("History DB<br/>Read-optimized")] end USER_A --> GW USER_B --> GW GW --> TXN TXN --> WALLET TXN --> LEDGER TXN --> ES TXN --> KAFKA WALLET --> WALLETDB WALLET --> CACHE LEDGER --> LEDGERDB KAFKA --> AUDIT KAFKA --> HISTORYDB AUDIT --> LEDGERDB AUDIT --> WALLETDB
2.2.3 Transfer Flow Overview (Happy Path)
Khi User A chuyển 100,000 VND cho User B:
1. User A → API Gateway: "Chuyển 100,000 VND cho User B" (kèm idempotency_key)
2. API Gateway: Kiểm tra auth, rate limit, idempotency_key (đã xử lý chưa?)
3. API Gateway → Transaction Service: Tạo transfer request
4. Transaction Service → Wallet Service: "Kiểm tra số dư A >= 100,000?"
5. Wallet Service: "Có, A có 500,000 VND"
6. Transaction Service → Event Store: Ghi event "TransferInitiated"
7. Transaction Service → Wallet Service: "Debit A 100,000 VND"
8. Wallet Service → Event Store: Ghi event "DebitCompleted"
9. Transaction Service → Wallet Service: "Credit B 100,000 VND"
10. Wallet Service → Event Store: Ghi event "CreditCompleted"
11. Transaction Service → Ledger Service: Ghi double-entry (Debit A, Credit B)
12. Transaction Service → Kafka: Publish "TransferCompleted" event
13. Kafka → Audit Service: Kiểm tra tính nhất quán
14. Kafka → History DB: Ghi lịch sử giao dịch cho A và B
15. Transaction Service → User A: "Chuyển tiền thành công!"
Step 3: Deep Dive — Thiết kế chi tiết
Đây là phần quan trọng nhất của chapter này. Alex Xu trình bày một hành trình từ giải pháp đơn giản nhất đến giải pháp production-ready, mỗi bước giải quyết một vấn đề cụ thể.
3.1 In-memory Solution — Tại sao không hoạt động
Ý tưởng đơn giản nhất: Lưu số dư trong memory (HashMap), khi chuyển tiền thì trừ A và cộng B.
| Vấn đề | Giải thích | Ví dụ |
|---|---|---|
| Race condition | 2 request đồng thời đọc cùng số dư, cả 2 đều thấy “đủ tiền”, cả 2 đều trừ | A có 100K, 2 giao dịch 80K cùng lúc → A bị trừ 160K → số dư âm |
| Data loss on crash | Server restart = mất hết số dư | MoMo crash → 35 triệu người dùng mất hết tiền |
| Không distributed | Một server duy nhất = single point of failure | Server đổ = toàn bộ hệ thống chết |
| Không có audit trail | Chỉ lưu state, không lưu lịch sử thay đổi | Không thể điều tra khi có tranh chấp |
| Không có durability | Memory là volatile — mất điện = mất data | Không thể chấp nhận cho hệ thống tài chính |
Bài học: In-memory solution chỉ phù hợp cho prototype hoặc bài tập. Hệ thống tài chính bắt buộc phải persist data và có audit trail.
3.2 Database-based Solution — Giải pháp đầu tiên thực sự
Ý tưởng: Dùng database (PostgreSQL) với ACID transaction để đảm bảo consistency.
Cách hoạt động: Trong cùng một database transaction:
- BEGIN TRANSACTION
- Kiểm tra số dư A >= X
- UPDATE wallet SET balance = balance - X WHERE user_id = A
- UPDATE wallet SET balance = balance + X WHERE user_id = B
- INSERT INTO ledger (debit A, credit B, amount X)
- COMMIT
Tại sao hoạt động tốt (cho single database):
| Đặc điểm ACID | Áp dụng cho wallet | Kết quả |
|---|---|---|
| Atomicity | Cả debit và credit hoặc cả hai thành công, hoặc cả hai rollback | Không bao giờ A mất tiền mà B không nhận |
| Consistency | Constraint: balance >= 0 | Không thể trừ nhiều hơn số dư |
| Isolation | 2 giao dịch đồng thời không xung đột | Không race condition |
| Durability | Data được lưu xuống disk, có WAL | Không mất data khi crash |
Giới hạn của database-based solution:
| Giới hạn | Giải thích | Khi nào gặp |
|---|---|---|
| Single database bottleneck | Toàn bộ giao dịch đi qua 1 DB | Khi scale quá 10K TPS |
| Vertical scaling limit | Không thể tăng mãi CPU/RAM của 1 server | Khi đạt giới hạn phần cứng |
| Không distributed | A và B phải nằm trên cùng 1 DB | Khi cần shard data theo user |
| Lock contention | Nhiều giao dịch cùng tác động 1 wallet gây blocking | ”Hot wallet” (ví nhận nhiều tiền) |
| Cross-region impossible | Transaction không thể span across regions | Khi cần multi-region deployment |
Aha Moment: Database-based solution là hoàn hảo cho hệ thống nhỏ (< 10K TPS, single region). Nhiều startup fintech bắt đầu với giải pháp này và nó hoạt động rất tốt. Vấn đề chỉ xảy ra khi scale — và đó là lúc cần chuyển sang event sourcing.
3.3 Distributed Transaction Challenge — Bài toán thực sự khó
Khi hệ thống lớn lên, wallet A và wallet B có thể nằm trên khác shard hoặc khác service. Lúc này, ACID transaction của single database không còn hoạt động.
Vì sao phải shard?
Tưởng tượng MoMo có 35 triệu người dùng. Một PostgreSQL instance không thể lưu 35 triệu wallet và xử lý 5 triệu giao dịch/ngày. Phải shard — chia wallet theo user_id ra nhiều database server.
Vấn đề: User A (shard 1) chuyển tiền cho User B (shard 2). Làm sao đảm bảo atomicity khi debit và credit nằm trên 2 database khác nhau?
3.3.1 Two-Phase Commit (2PC) — Và tại sao nó có vấn đề
2PC là giải pháp “kinh điển” cho distributed transaction:
| Phase | Hành động | Giải thích |
|---|---|---|
| Phase 1: Prepare | Coordinator hỏi tất cả participant: “Sẵn sàng commit chưa?” | Shard 1: “Sẵn sàng debit A”, Shard 2: “Sẵn sàng credit B” |
| Phase 2: Commit | Nếu tất cả “sẵn sàng” → Coordinator nói: “Commit!” | Cả 2 shard commit đồng thời |
| Rollback | Nếu bất kỳ participant nào “không sẵn sàng” → “Rollback!” | Cả 2 shard rollback |
Vấn đề của 2PC trong thực tế:
| Vấn đề | Giải thích | Hậu quả |
|---|---|---|
| Coordinator failure | Coordinator crash sau Phase 1, trước Phase 2 | Các participant “treo” — không biết commit hay rollback |
| Blocking | Participant phải giữ lock cho đến khi nhận lệnh từ coordinator | Các giao dịch khác bị block, throughput giảm mạnh |
| Latency cao | 2 round-trip giữa coordinator và tất cả participant | Latency tăng gấp đôi so với single DB |
| Không fault-tolerant | Một participant crash = toàn bộ transaction bị block | Không phù hợp cho hệ thống high-availability |
| Network partition | Coordinator và participant không liên lạc được | Trạng thái không xác định — nguy hiểm nhất |
Key Insight: 2PC làm việc tốt trong cùng datacenter với ít participant. Nhưng khi cross-region hoặc có nhiều service, 2PC trở thành bottleneck và single point of failure. Đây là lý do tại sao các hệ thống tài chính lớn không dùng 2PC mà chuyển sang event sourcing + saga.
3.4 Event Sourcing Approach — THE Key Pattern
Đây là trái tim của chapter này. Event sourcing là pattern thay đổi cách ta suy nghĩ về data storage.
3.4.1 Triết lý core: Events là source of truth
Cách truyền thống (state-based):
- Lưu trạng thái hiện tại của wallet:
balance = 400,000 VND - Khi chuyển tiền: UPDATE balance
Cách event sourcing (event-based):
- Lưu mọi sự kiện đã xảy ra:
WalletCreated: user=A, initial_balance=0CreditCompleted: user=A, amount=500,000, source=bank_depositDebitCompleted: user=A, amount=100,000, target=user_B, transfer_id=T001
- Balance = replay tất cả events: 0 + 500,000 - 100,000 = 400,000 VND
| So sánh | State-based | Event Sourcing |
|---|---|---|
| Lưu gì | Trạng thái hiện tại | Mọi sự kiện đã xảy ra |
| Update | Ghi đè state cũ | Append event mới |
| Lịch sử | Mất (chỉ có state mới nhất) | Có toàn bộ — từ ngày đầu tiên |
| Audit | Cần thêm audit log riêng | Event log chính là audit trail |
| Debug | Khó — chỉ thấy state hiện tại | Dễ — replay events để thấy chính xác chuyện gì đã xảy ra |
| Rebuild | Không thể | Có thể rebuild state bất kỳ lúc nào |
| Storage | Ít hơn | Nhiều hơn (nhưng storage rẻ) |
3.4.2 Events trong Digital Wallet
Mỗi giao dịch chuyển tiền tạo ra chuỗi events:
| Event | Mô tả | Data |
|---|---|---|
| TransferInitiated | Giao dịch được tạo | transfer_id, from_wallet, to_wallet, amount, timestamp, idempotency_key |
| BalanceChecked | Kiểm tra số dư thành công | transfer_id, wallet_id, current_balance, required_amount |
| DebitCompleted | Đã trừ tiền từ wallet A | transfer_id, wallet_id, amount, balance_after |
| CreditCompleted | Đã cộng tiền vào wallet B | transfer_id, wallet_id, amount, balance_after |
| TransferCompleted | Giao dịch hoàn tất | transfer_id, status=SUCCESS, timestamp |
| TransferFailed | Giao dịch thất bại | transfer_id, status=FAILED, reason, timestamp |
| DebitReversed | Hoàn tiền lại cho A (compensating) | transfer_id, wallet_id, amount, reason |
3.4.3 Balance = Replay events (hoặc Materialized View)
Cách 1: Replay tất cả events
Để tính số dư wallet A, đọc tất cả events liên quan đến wallet A và “replay” từ đầu:
Event 1: WalletCreated(A) → balance = 0
Event 2: CreditCompleted(A, +500,000) → balance = 500,000
Event 3: DebitCompleted(A, -100,000) → balance = 400,000
Event 4: CreditCompleted(A, +200,000) → balance = 600,000
Event 5: DebitCompleted(A, -50,000) → balance = 550,000
→ Current balance of A = 550,000 VND
Vấn đề: Nếu wallet A có 10,000 events, mỗi lần query balance phải replay 10,000 events = chậm.
Cách 2: Materialized View (balance table)
Duy trì một bảng wallet_balance được cập nhật mỗi khi có event mới. Đây là “materialized view” của event stream.
| Thành phần | Vai trò |
|---|---|
| Event Store | Source of truth — lưu toàn bộ events |
| Materialized View | Derived data — balance được tính từ events |
| Event Processor | Consumer đọc events và cập nhật materialized view |
Aha Moment: Materialized view có thể sai (bug, crash giữa chừng). Nhưng vì ta có toàn bộ events, ta có thể xóa materialized view và rebuild lại từ đầu. Đây là sức mạnh của event sourcing — source of truth không bao giờ mất.
3.4.4 Idempotency via event_id
Mỗi event có một event_id unique (UUID hoặc hash của content). Mỗi transfer có một idempotency_key do client tạo.
Flow đảm bảo idempotency:
1. Client gửi: POST /transfer {idempotency_key: "abc-123", from: A, to: B, amount: 100K}
2. Server check: "abc-123" đã tồn tại trong event store chưa?
- Nếu có → Trả về kết quả cũ (không xử lý lại)
- Nếu chưa → Xử lý bình thường, lưu event với idempotency_key
| Tình huống | Không có idempotency | Có idempotency |
|---|---|---|
| Client gửi 1 lần | OK | OK |
| Client gửi 2 lần (retry do timeout) | A bị trừ 2 lần! | Lần 2 được skip, trả về kết quả lần 1 |
| Client gửi 5 lần (retry loop) | A bị trừ 5 lần! | Chỉ xử lý 1 lần |
3.4.5 Event Store — Append-only Log
Event store là nơi lưu trữ tất cả events. Nó có những đặc điểm đặc biệt:
| Đặc điểm | Giải thích | Tại sao quan trọng |
|---|---|---|
| Append-only | Chỉ thêm event mới, không bao giờ update hay delete | Đảm bảo immutability — audit trail không thể bị sửa |
| Ordered | Events được sắp xếp theo thời gian (hoặc sequence number) | Replay phải theo đúng thứ tự |
| Persistent | Lưu trên disk với replication | Không mất data |
| Queryable | Có thể query events theo wallet_id, transfer_id, time range | Hỗ trợ transaction history và audit |
Lựa chọn công nghệ cho Event Store:
| Công nghệ | Ưu điểm | Nhược điểm | Khi nào dùng |
|---|---|---|---|
| Kafka | Throughput cao, distributed, mature | Khó query theo criteria phức tạp, retention có limit | Event streaming giữa services |
| EventStoreDB | Thiết kế dành riêng cho event sourcing, projections, subscriptions | Ít mature hơn, community nhỏ hơn | Event sourcing là core của hệ thống |
| PostgreSQL (append-only table) | Quen thuộc, SQL query, ACID | Throughput thấp hơn Kafka, phải tự implement event sourcing logic | Hệ thống nhỏ-vừa, team quen PostgreSQL |
| DynamoDB (append-only) | Serverless, auto-scaling, high throughput | Vendor lock-in, query hạn chế | AWS ecosystem |
Thực tế: Nhiều hệ thống dùng kết hợp: PostgreSQL làm event store chính (ACID, queryable) + Kafka làm event bus (distribute events đến các consumer). Đây là pattern phổ biến nhất.
3.4.6 CQRS — Command Query Responsibility Segregation
CQRS là pattern tách write (command) và read (query) thành hai hệ thống riêng biệt.
Tại sao cần CQRS cho digital wallet?
| Vấn đề | Giải thích |
|---|---|
| Write và read có yêu cầu khác nhau | Write cần consistency cao (ACID). Read cần throughput cao (1000x nhiều hơn write) |
| Event store không tối ưu cho read | Replay 10,000 events để lấy balance = chậm. Cần materialized view |
| Scale độc lập | Write ít nhưng quan trọng. Read nhiều nhưng có thể chấp nhận eventually consistent |
| Model khác nhau | Write model: events. Read model: bảng số dư, lịch sử giao dịch — cấu trúc khác hoàn toàn |
flowchart LR subgraph "Command Side (Write)" CMD["Transfer Command"] VALIDATE["Validate<br/>Balance Check"] EVTSTORE["Event Store<br/>(Source of Truth)"] end subgraph "Event Bus" KAFKA["Kafka<br/>Event Distribution"] end subgraph "Query Side (Read)" BALVIEW["Balance View<br/>(Materialized)"] HISTVIEW["History View<br/>(Transaction List)"] CACHE["Redis Cache<br/>(Hot Balance)"] end subgraph "Consumers" PROJ_BAL["Balance<br/>Projector"] PROJ_HIST["History<br/>Projector"] end CMD --> VALIDATE VALIDATE --> EVTSTORE EVTSTORE --> KAFKA KAFKA --> PROJ_BAL KAFKA --> PROJ_HIST PROJ_BAL --> BALVIEW PROJ_BAL --> CACHE PROJ_HIST --> HISTVIEW USER_WRITE["User: Transfer"] --> CMD USER_READ["User: Check Balance"] --> CACHE USER_HIST["User: View History"] --> HISTVIEW
Command side (Write path):
- Nhận transfer command
- Validate (balance check, fraud check)
- Ghi events vào Event Store (source of truth)
- Publish events lên Kafka
Query side (Read path):
- Kafka consumer (projector) đọc events
- Cập nhật materialized views (balance table, history table)
- Cập nhật Redis cache
- User query đọc từ materialized view hoặc cache
| Đặc điểm | Command Side | Query Side |
|---|---|---|
| Data model | Events (append-only) | Materialized views (tables, cache) |
| Consistency | Strong (ACID) | Eventually consistent |
| Throughput | Thấp hơn (cần lock, validate) | Cao hơn (read-only, có cache) |
| Scale | Khó scale (consistency requirement) | Dễ scale (thêm read replica, cache) |
| Latency | Cao hơn (write to disk) | Thấp hơn (read from cache) |
Aha Moment: CQRS cho phép bạn scale read và write độc lập. Balance inquiry (read) có thể nhiều gấp 100 lần so với transfer (write). Với CQRS, bạn thêm Redis replica và read DB replica để scale read mà không ảnh hưởng write path.
3.4.7 Rebuilding State từ Events và Snapshots
Rebuilding state: Vì mọi event được lưu, bạn có thể xóa toàn bộ materialized view và rebuild lại từ đầu bằng cách replay events.
Khi nào cần rebuild?
- Bug trong projector logic → sửa bug → rebuild
- Thêm materialized view mới (ví dụ: thêm báo cáo tháng)
- Data corruption trong read model
- Migrate sang schema mới
Vấn đề với replay: Wallet có 10 triệu events → replay mất hàng giờ.
Giải pháp: Snapshots
| Khía cạnh | Giải thích |
|---|---|
| Snapshot là gì | ”Ảnh chụp” của state tại một thời điểm cụ thể |
| Ví dụ | Snapshot tại event #9,000,000: {wallet_A: {balance: 550,000, event_seq: 9000000}} |
| Khi rebuild | Load snapshot #9,000,000 → replay 1,000,000 events còn lại (thay vì 10,000,000) |
| Tạo khi nào | Định kỳ (mỗi 10,000 events, hoặc mỗi 1 giờ) |
| Lưu ở đâu | Cùng event store hoặc object storage (S3) |
Snapshot strategy:
| Strategy | Tần suất | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Every N events | Mỗi 10,000 events | Dự đoán được rebuild time | Hot wallet có snapshot nhiều, cold wallet ít |
| Time-based | Mỗi 1 giờ | Đơn giản | Không tối ưu cho wallet có nhiều events |
| On-demand | Khi rebuild cần | Ít tốn storage | Phải đợi lâu khi rebuild |
| Hybrid | Mỗi 10,000 events HOẶC mỗi 6 giờ (cái nào đến trước) | Cân bằng | Phức tạp hơn để implement |
3.5 Saga Pattern cho Distributed Wallet
Khi wallet A và wallet B nằm trên khác shard hoặc khác service, ta không thể dùng single database transaction. Saga pattern là giải pháp.
3.5.1 Saga là gì?
Saga là chuỗi các local transaction, mỗi transaction cập nhật một service/database. Nếu một step thất bại, thực hiện compensating actions để undo các step trước đó.
3.5.2 Transfer Saga Flow
flowchart TD START["Transfer Request<br/>A → B, 100,000 VND"] --> STEP1 subgraph "Step 1: Debit" STEP1["Debit Wallet A<br/>-100,000 VND"] STEP1 -->|Success| STEP1_OK["Event: DebitCompleted"] STEP1 -->|Fail: Insufficient balance| STEP1_FAIL["Event: TransferFailed<br/>Reason: Insufficient funds"] end STEP1_OK --> STEP2 subgraph "Step 2: Credit" STEP2["Credit Wallet B<br/>+100,000 VND"] STEP2 -->|Success| STEP2_OK["Event: CreditCompleted"] STEP2 -->|Fail: Wallet B frozen| STEP2_FAIL["Event: CreditFailed"] end STEP2_OK --> COMPLETE["Event: TransferCompleted<br/>Status: SUCCESS"] STEP2_FAIL --> COMPENSATE subgraph "Compensating Action" COMPENSATE["Reverse Debit A<br/>+100,000 VND"] COMPENSATE --> COMPENSATE_OK["Event: DebitReversed"] COMPENSATE_OK --> FAILED["Event: TransferFailed<br/>Reason: Credit failed, debit reversed"] end STEP1_FAIL --> ABORT["Transfer Aborted<br/>No money moved"]
3.5.3 Saga Orchestration vs Choreography
| Đặc điểm | Orchestration | Choreography |
|---|---|---|
| Cách hoạt động | Một “orchestrator” điều phối từng step | Mỗi service tự quyết định hành động tiếp theo dựa trên event |
| Control flow | Tập trung tại orchestrator | Phân tán giữa các service |
| Dễ hiểu | Có — đọc orchestrator là hiểu toàn bộ flow | Khó — phải xem nhiều service để hiểu flow |
| Dễ debug | Có — log tập trung | Khó — log phân tán |
| Coupling | Orchestrator biết tất cả services | Services chỉ biết events, không biết nhau |
| Single point of failure | Orchestrator là SPOF | Không có SPOF |
| Dùng cho wallet | Nên dùng — flow phải chính xác, dễ audit | Phù hợp hơn cho flow đơn giản |
Khuyến nghị của Alex Xu: Với digital wallet, dùng orchestration-based saga vì:
- Transfer flow có thứ tự nghiêm ngặt (debit trước, credit sau)
- Cần biết chính xác trạng thái của mỗi step
- Compensating action phải được điều phối chính xác
- Audit requirement cao — cần log tập trung
3.5.4 Saga State Machine
Mỗi transfer được mô hình hóa như state machine:
| State | Mô tả | Chuyển trạng thái |
|---|---|---|
| INITIATED | Transfer vừa được tạo | → DEBITING |
| DEBITING | Đang trừ tiền từ wallet A | → DEBITED (thành công) hoặc → FAILED (thất bại) |
| DEBITED | Đã trừ tiền, đang credit | → CREDITING |
| CREDITING | Đang cộng tiền vào wallet B | → COMPLETED (thành công) hoặc → COMPENSATING (thất bại) |
| COMPLETED | Giao dịch hoàn tất thành công | (terminal state) |
| COMPENSATING | Đang hoàn tiền lại cho A | → COMPENSATED |
| COMPENSATED | Đã hoàn tiền, giao dịch thất bại | (terminal state) |
| FAILED | Thất bại từ đầu (balance không đủ) | (terminal state) |
Aha Moment: State machine đảm bảo mỗi transfer chỉ có thể ở một trạng thái tại một thời điểm, và các chuyển trạng thái là deterministic. Điều này cực kỳ quan trọng cho audit và debug.
3.6 Exactly-once Guarantee — Làm sao đảm bảo?
“Exactly-once” là yêu cầu khó nhất trong distributed system. Thực tế, không có exactly-once delivery trong mạng. Nhưng ta có thể đạt được effectively exactly-once bằng cách kết hợp:
Công thức: Idempotency + Event Dedup + At-least-once Delivery = Effectively Exactly-once
| Thành phần | Cách hoạt động | Giải quyết vấn đề gì |
|---|---|---|
| Idempotency key | Client gửi kèm UUID unique cho mỗi transfer. Server check trước khi xử lý | Client retry không gây duplicate |
| Event deduplication | Mỗi event có unique event_id. Consumer check trước khi process | Event được deliver nhiều lần nhưng chỉ process 1 lần |
| At-least-once delivery | Kafka đảm bảo mỗi message được deliver ít nhất 1 lần (ack + retry) | Không mất event |
Ví dụ cụ thể:
Tình huống: User A gửi "Chuyển 100K cho B". Network timeout. Client retry.
Lần 1: POST /transfer {idempotency_key: "txn-abc-123", from: A, to: B, amount: 100K}
→ Server xử lý thành công, lưu event với idempotency_key "txn-abc-123"
→ Response bị mất trên đường về (network issue)
Lần 2: POST /transfer {idempotency_key: "txn-abc-123", from: A, to: B, amount: 100K}
→ Server check: "txn-abc-123" đã tồn tại!
→ Trả về kết quả cũ: "Transfer thành công" (không xử lý lại)
Kết quả: A chỉ bị trừ 100K một lần. B chỉ nhận 100K một lần. Chính xác!
| Nếu không có idempotency | Nếu có idempotency |
|---|---|
| Lần 1: Trừ A 100K, Cộng B 100K | Lần 1: Trừ A 100K, Cộng B 100K |
| Lần 2: Trừ A 100K nữa, Cộng B 100K nữa | Lần 2: Skip, trả về kết quả cũ |
| Tổng: A mất 200K, B nhận 200K — SAI | Tổng: A mất 100K, B nhận 100K — ĐÚNG |
3.7 Handling Failures — Xử lý lỗi
Trong hệ thống phân tán, lỗi là chuyện bình thường, không phải ngoại lệ. Ta phải thiết kế hệ thống để sống chung với lỗi.
3.7.1 Failure Scenarios và xử lý
| Scenario | Giải thích | Xử lý |
|---|---|---|
| Debit thành công, credit timeout | A đã bị trừ, nhưng không biết B đã nhận chưa | Check trạng thái credit → nếu thất bại → compensate (hoàn tiền A) |
| Debit thành công, credit service down | A đã bị trừ, credit service không phản hồi | Retry với exponential backoff → nếu quá max retry → compensate |
| Orchestrator crash giữa chừng | Đang xử lý thì orchestrator die | Orchestrator mới khởi động → đọc saga state → tiếp tục từ step cuối |
| Database crash | Database không ghi được event | Retry → nếu vẫn fail → reject transfer |
| Kafka consumer lag | Consumer xử lý chậm, balance view bị stale | Alert → scale consumer → user thấy balance cũ nhưng vẫn đúng |
| Network partition | 2 shard không liên lạc được | Saga bị “treo” → timeout → compensate → retry sau |
3.7.2 Retry Strategy
| Aspect | Chi tiết |
|---|---|
| Retry policy | Exponential backoff: 1s → 2s → 4s → 8s → 16s |
| Max retries | 5 lần (tổng ~31 giây) |
| Jitter | Thêm random delay để tránh “thundering herd” |
| Idempotent | Mỗi retry gửi cùng idempotency_key → không gây duplicate |
| Timeout per step | 5 giây cho mỗi saga step |
3.7.3 Dead Letter Queue (DLQ)
Khi một giao dịch thất bại sau tất cả retry:
| Bước | Hành động |
|---|---|
| 1 | Transfer bị đẩy vào Dead Letter Queue |
| 2 | Alert team (PagerDuty, Slack) |
| 3 | Engineer điều tra nguyên nhân |
| 4 | Fix lỗi |
| 5 | Replay từ DLQ (xử lý lại giao dịch) |
Key Insight: DLQ là “bảng cấp cứu” cho giao dịch bị lỗi. Không bao giờ để giao dịch “im lặng thất bại”. Mỗi giao dịch phải có trạng thái cuối cùng: SUCCESS hoặc FAILED với lý do rõ ràng.
3.7.4 Failure Handling Flow
flowchart TD REQ["Transfer Request"] --> PROCESS["Process Transfer"] PROCESS -->|Success| DONE["TransferCompleted"] PROCESS -->|Timeout / Error| RETRY{"Retry?<br/>count < max_retry?"} RETRY -->|Yes| WAIT["Wait<br/>(Exponential Backoff)"] WAIT --> PROCESS RETRY -->|No: max retry reached| CHECK{"Check partial state"} CHECK -->|Debit done, credit not| COMPENSATE["Compensate:<br/>Reverse Debit"] CHECK -->|Nothing done| REJECT["Reject Transfer"] COMPENSATE --> DLQ["Dead Letter Queue<br/>+ Alert Team"] REJECT --> DLQ DLQ --> MANUAL["Manual Review<br/>by Engineer"] MANUAL --> REPLAY["Replay from DLQ"] REPLAY --> PROCESS
3.8 Audit Trail — Mọi sự kiện là bằng chứng
Một trong những ưu điểm lớn nhất của event sourcing là audit trail miễn phí.
3.8.1 Tại sao audit trail quan trọng?
| Lý do | Ví dụ |
|---|---|
| Dispute resolution | User A nói “Tôi không chuyển tiền này!” → Kiểm tra event log |
| Regulatory compliance | Ngân hàng Nhà nước yêu cầu kiểm tra giao dịch → Có sẵn |
| Fraud detection | Phát hiện pattern bất thường từ event log |
| Bug investigation | Tìm chính xác lúc nào, event nào gây lỗi |
| Financial reconciliation | Đối soát số dư với double-entry ledger |
3.8.2 Event log vs Double-entry Ledger
Event sourcing tạo ra event log (danh sách events). Nhưng hệ thống tài chính còn cần double-entry ledger — mỗi giao dịch phải có debit entry và credit entry với tổng bằng 0.
| Giao dịch | Debit Entry | Credit Entry | Tổng |
|---|---|---|---|
| A → B, 100K | Wallet A: -100,000 | Wallet B: +100,000 | 0 |
| C → D, 50K | Wallet C: -50,000 | Wallet D: +50,000 | 0 |
Reconciliation: Định kỳ (mỗi giờ, mỗi ngày), Audit Service so sánh:
- Event log: Tổng số events, tổng số tiền di chuyển
- Double-entry ledger: Tổng debit = Tổng credit? Mỗi giao dịch có cả 2 entry?
- Materialized balance: Tổng tất cả balance = Tổng tiền trong hệ thống?
Nếu bất kỳ phép kiểm tra nào không khớp → Alert ngay lập tức.
3.8.3 Reconciliation Pipeline
flowchart LR subgraph "Data Sources" EVT["Event Store"] LEDGER["Double-entry<br/>Ledger"] BAL["Balance<br/>Materialized View"] end subgraph "Reconciliation Engine" RECON["Reconciliation<br/>Service"] RULE["Rule Engine<br/>Comparison Rules"] end subgraph "Output" OK["Match ✓<br/>All consistent"] MISMATCH["Mismatch ✗<br/>Discrepancy Found"] end subgraph "Action" ALERT["PagerDuty Alert"] REPORT["Discrepancy Report"] AUTO_FIX["Auto-fix<br/>(if safe)"] end EVT --> RECON LEDGER --> RECON BAL --> RECON RECON --> RULE RULE --> OK RULE --> MISMATCH MISMATCH --> ALERT MISMATCH --> REPORT MISMATCH --> AUTO_FIX
3.9 Balance Cache — Redis cho Real-time Query
3.9.1 Tại sao cần cache?
| Metric | Không có cache | Có Redis cache |
|---|---|---|
| Balance query latency | 10-50ms (database read) | 1-2ms (Redis read) |
| Database load | Cao (mỗi balance check = 1 DB query) | Thấp (chỉ khi cache miss) |
| Scale read | Khó (database bottleneck) | Dễ (thêm Redis replica) |
3.9.2 Cache Invalidation Strategy
| Strategy | Mô tả | Dùng khi |
|---|---|---|
| Write-through | Cập nhật cache ngay khi ghi event | Balance phải chính xác ngay sau transfer |
| Event-driven | Kafka consumer cập nhật cache khi nhận event | Chấp nhận delay vài ms |
| TTL-based | Cache hết hạn sau N giây, đọc lại từ DB | Balance hiển thị (không cần 100% real-time) |
Chiến lược khuyến nghị: Write-through + TTL backup
- Khi DebitCompleted/CreditCompleted → cập nhật Redis ngay lập tức
- Đặt TTL 60 giây cho mỗi cache entry → nếu write-through fail, cache sẽ expire và đọc lại từ DB
- Balance check trước khi debit → đọc từ database (không phải cache) để đảm bảo strong consistency
Aha Moment: Balance hiển thị cho user có thể đọc từ cache (eventually consistent — chấp nhận sai lệch vài ms). Nhưng balance kiểm tra trước khi trừ tiền phải đọc từ database (strong consistency). Đây là ví dụ kinh điển của việc dùng consistency level khác nhau cho các use case khác nhau.
3.10 Historical Balance — Số dư tại một thời điểm trong quá khứ
3.10.1 Tại sao cần historical balance?
| Use case | Ví dụ |
|---|---|
| Statement generation | ”Số dư của tôi lúc 23:59 ngày 31/12/2025 là bao nhiêu?” |
| Dispute resolution | ”Lúc 14:30 số dư của tôi là 500K, tại sao giao dịch 200K bị từ chối?” |
| Regulatory reporting | Báo cáo số dư cuối ngày/cuối tháng cho ngân hàng nhà nước |
| Audit | Kiểm tra số dư tại bất kỳ thời điểm nào |
3.10.2 Cách implement
| Cách | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Event replay | Replay events từ đầu đến thời điểm cần | Chính xác tuyệt đối | Chậm với nhiều events |
| Snapshot + replay | Load snapshot gần nhất trước thời điểm, replay events còn lại | Nhanh hơn | Cần lưu nhiều snapshots |
| Temporal table | Database lưu mỗi phiên bản của row (valid_from, valid_to) | Query nhanh | Storage lớn, schema phức tạp |
| Daily snapshot | Lưu số dư cuối mỗi ngày | Đơn giản, query nhanh | Chỉ có độ chính xác theo ngày |
Khuyến nghị: Daily snapshot + event replay khi cần chính xác
- Mỗi ngày 00:00: Chụp snapshot số dư của tất cả wallets
- Khi user hỏi “số dư lúc 14:30 ngày 15/3”: Load snapshot ngày 15/3 (00:00) → replay events từ 00:00 đến 14:30
- Nhanh hơn replay từ đầu rất nhiều
3. Estimation — Ước lượng hiệu năng
3.1 Transaction QPS
3.2 Event Store Growth
Mỗi transfer tạo ra trung bình 5 events (Initiated, BalanceChecked, Debit, Credit, Completed).
Mỗi event trung bình 500 bytes (JSON với metadata):
Với retention 7 năm (yêu cầu pháp lý):
Nhận xét: 7 TB là hoàn toàn quản lý được với PostgreSQL hoặc DynamoDB. Không cần giải pháp lưu trữ đặc biệt.
3.3 Balance Query QPS
User check balance thường xuyên hơn chuyển tiền (trung bình 10 lần check balance cho mỗi 1 lần transfer):
Với Redis cache (P99 latency < 2ms), đây là con số nhỏ — một Redis instance có thể xử lý 100,000+ QPS.
3.4 Snapshot Frequency
Nếu chọn snapshot mỗi 10,000 events cho mỗi wallet:
Như vậy, với strategy “mỗi 10,000 events”, hầu hết wallet chỉ cần 1 snapshot trong 5 năm. Nên kết hợp với daily snapshot để tối ưu rebuild time.
3.5 Ledger Storage
Mỗi transaction tạo 2 ledger entries (debit + credit), mỗi entry 200 bytes:
3.6 Bandwidth
Kết luận: Digital wallet là bài toán consistency-intensive, không phải throughput-intensive hay bandwidth-intensive. Thách thức không nằm ở scale, mà nằm ở đảm bảo chính xác tuyệt đối với mỗi giao dịch.
4. Security — Bảo mật hệ thống tài chính
4.1 Encryption of Financial Data
| Tầng | Biện pháp | Giải thích |
|---|---|---|
| Data at rest | AES-256 encryption cho database và event store | Dù ai truy cập disk cũng không đọc được data |
| Data in transit | TLS 1.3 cho mọi giao tiếp giữa services | Không thể nghe lén network traffic |
| Field-level encryption | Encrypt trường nhạy cảm (balance, account_id) riêng | Dù database bị compromise, data vẫn an toàn |
| Key management | AWS KMS hoặc HashiCorp Vault | Không lưu key trong code hoặc config file |
| Encryption key rotation | Thay key định kỳ (mỗi 90 ngày) | Giảm thiệt hại nếu key bị lộ |
4.2 Fraud Detection
| Kỹ thuật | Mô tả | Ví dụ |
|---|---|---|
| Velocity check | Giới hạn số giao dịch trong khoảng thời gian | Tối đa 10 giao dịch/phút, tối đa 50 triệu/ngày |
| Unusual pattern detection | Phát hiện giao dịch bất thường so với lịch sử | User thường chuyển 50K-200K, đột nhiên chuyển 50 triệu |
| Geo-anomaly | Giao dịch từ địa điểm bất thường | Đang ở Hà Nội mà có giao dịch từ Cambodia |
| Device fingerprinting | Giao dịch từ thiết bị lạ | Giao dịch từ thiết bị mới chưa bao giờ dùng |
| Graph analysis | Phát hiện mạng lưới chuyển tiền đáng ngờ | 10 tài khoản mới chuyển tiền vòng quanh (money laundering) |
| ML-based scoring | Chấm điểm rủi ro cho mỗi giao dịch | Score > 0.8 → block và yêu cầu xác minh |
4.3 AML (Anti-Money Laundering) Compliance
| Yêu cầu | Mô tả |
|---|---|
| KYC (Know Your Customer) | Xác minh danh tính trước khi cho phép giao dịch lớn |
| Transaction monitoring | Theo dõi giao dịch lớn (trên 200 triệu VND phải báo cáo NHNN) |
| Suspicious Activity Report (SAR) | Báo cáo giao dịch đáng ngờ cho cơ quan chức năng |
| Sanction screening | Kiểm tra danh sách đen quốc tế (OFAC, UN) |
| Record keeping | Lưu trữ giao dịch tối thiểu 5-7 năm |
4.4 Authentication cho Transfers
| Bước | Mô tả | Tại sao |
|---|---|---|
| Session auth | JWT token cho mỗi request | Xác nhận danh tính user |
| 2FA cho transfer lớn | OTP qua SMS/app cho giao dịch trên ngưỡng (5 triệu VND) | Ngăn chặn unauthorized transfer |
| PIN/Biometric | Yêu cầu PIN hoặc vân tay trước mỗi transfer | Bảo vệ ngay cả khi điện thoại bị mất |
| Device binding | Ràng buộc ví với thiết bị cụ thể | Không thể truy cập ví từ thiết bị lạ |
4.5 PCI-DSS (nếu liên kết thẻ)
Nếu digital wallet cho phép liên kết thẻ ngân hàng (như MoMo liên kết Visa/Mastercard):
| Yêu cầu PCI-DSS | Mô tả |
|---|---|
| Không lưu card number trên server | Dùng tokenization (thay card number bằng token) |
| Network segmentation | Tách mạng xử lý thẻ riêng |
| Penetration testing | Test bảo mật định kỳ (hàng quý) |
| Access control | Chỉ người được ủy quyền mới truy cập cardholder data |
| Logging & monitoring | Ghi log mọi truy cập vào cardholder data |
Key Insight: Security trong fintech không chỉ là kỹ thuật. Nó bao gồm process (KYC, SAR), compliance (PCI-DSS, AML), và monitoring (fraud detection). Tham chiếu Tuan-15-Data-Security-Encryption và Tuan-14-AuthN-AuthZ-Security để hiểu sâu hơn.
5. DevOps — Giám sát và vận hành
5.1 Key Metrics cần giám sát
| Metric | Mô tả | Threshold cảnh báo |
|---|---|---|
| Transaction success rate | Tỷ lệ giao dịch thành công | < 99.9% → P1 alert |
| Transaction latency (P50, P99) | Thời gian xử lý giao dịch | P99 > 500ms → investigate |
| Event processing lag | Độ trễ giữa ghi event và cập nhật materialized view | > 5 giây → scale consumer |
| Balance consistency check | Kết quả reconciliation định kỳ | Any mismatch → P0 alert (critical) |
| Dead letter queue depth | Số giao dịch bị stuck | > 0 trong 5 phút → investigate |
| Kafka consumer lag | Số message chưa được xử lý | > 10,000 → scale consumer |
| Snapshot freshness | Thời gian từ snapshot gần nhất | > 24h → tạo snapshot mới |
| Cache hit rate | Tỷ lệ balance query hit cache | < 90% → kiểm tra cache strategy |
| Error rate by type | Phân loại lỗi: timeout, insufficient balance, system error | System error > 0.1% → investigate |
5.2 Reconciliation Schedule
| Loại | Tần suất | Mô tả |
|---|---|---|
| Real-time | Mỗi giao dịch | Kiểm tra debit + credit = 0 cho mỗi transfer |
| Micro-batch | Mỗi 5 phút | So sánh event count với ledger entry count |
| Hourly | Mỗi giờ | Tổng balance tất cả wallets = tổng tiền trong hệ thống |
| Daily | Mỗi ngày 01:00 | Full reconciliation: event store vs ledger vs balance view |
| Monthly | Cuối tháng | Báo cáo cho compliance và kế toán |
5.3 Alerting Hierarchy
| Level | Tình huống | Hành động |
|---|---|---|
| P0 (Critical) | Balance mismatch phát hiện bởi reconciliation | Tất cả engineer on-call, dừng giao dịch mới nếu cần, fix ngay |
| P1 (High) | Transaction success rate < 99.9% | On-call engineer điều tra trong 15 phút |
| P2 (Medium) | Event processing lag > 10 giây | Engineer điều tra trong 1 giờ |
| P3 (Low) | Cache hit rate giảm | Xem xét trong sprint tiếp theo |
5.4 Disaster Recovery
| Scenario | RTO | RPO | Strategy |
|---|---|---|---|
| Single server failure | < 1 phút | 0 (no data loss) | Auto-failover (database replica, service restart) |
| Database failure | < 5 phút | 0 | Synchronous replication + auto-failover |
| Datacenter failure | < 30 phút | < 1 giây | Cross-region replication, DNS failover |
| Event store corruption | < 2 giờ | 0 | Backup + replay từ backup |
5.5 Deployment Strategy
| Strategy | Mô tả | Áp dụng cho wallet |
|---|---|---|
| Blue-green deployment | 2 môi trường giống hệt, chuyển traffic khi sẵn sàng | Cho stateless services (API gateway, fraud detection) |
| Canary release | Deploy cho 1-5% traffic trước, rồi mở rộng | Cho wallet service (cần kiểm tra consistency trước khi rollout) |
| Feature flag | Bật/tắt feature không cần deploy lại | Cho tính năng mới (new transfer type, new validation rule) |
Tham chiếu: Tuan-13-Monitoring-Observability để hiểu sâu về monitoring và alerting strategy.
6. Architecture Diagrams (Mermaid)
6.1 Event Sourcing Flow chi tiết
sequenceDiagram participant Client participant API as API Gateway participant TXN as Transaction Service participant ES as Event Store participant WS as Wallet Service participant KAFKA as Kafka participant PROJ as Balance Projector participant CACHE as Redis Cache participant LEDGER as Ledger Service Client->>API: POST /transfer<br/>{idempotency_key, from, to, amount} API->>API: Check idempotency_key API->>TXN: Forward request TXN->>ES: Write: TransferInitiated TXN->>WS: Check balance(from_wallet) WS-->>TXN: balance >= amount ✓ TXN->>ES: Write: DebitCompleted TXN->>WS: Debit(from_wallet, amount) WS->>CACHE: Invalidate from_wallet cache TXN->>ES: Write: CreditCompleted TXN->>WS: Credit(to_wallet, amount) WS->>CACHE: Invalidate to_wallet cache TXN->>ES: Write: TransferCompleted TXN->>LEDGER: Record double-entry TXN->>KAFKA: Publish TransferCompleted KAFKA->>PROJ: Consume event PROJ->>CACHE: Update balance cache TXN-->>Client: Transfer successful
6.2 CQRS Read/Write Separation chi tiết
flowchart TB subgraph "Write Path (Command)" W_CLIENT["Client:<br/>Transfer Request"] W_VALIDATE["Validation<br/>• Balance check (from DB)<br/>• Fraud check<br/>• Idempotency check"] W_EVENT["Event Store<br/>(Append-only, Immutable)<br/>Source of Truth"] W_DB["Wallet DB<br/>(State Update)"] end subgraph "Event Distribution" KAFKA["Kafka<br/>Event Topics:<br/>• wallet.events<br/>• transfer.events"] end subgraph "Read Path (Query)" R_BAL_PROJ["Balance Projector<br/>Consume events →<br/>Update balance view"] R_HIST_PROJ["History Projector<br/>Consume events →<br/>Update history view"] R_CACHE["Redis<br/>Balance Cache<br/>(P99 < 2ms)"] R_BAL_DB["Balance View DB<br/>(Read-optimized)"] R_HIST_DB["History View DB<br/>(Read-optimized,<br/>Paginated)"] end subgraph "Read Clients" R_BAL_CLIENT["Client:<br/>Check Balance"] R_HIST_CLIENT["Client:<br/>View History"] end W_CLIENT --> W_VALIDATE W_VALIDATE --> W_EVENT W_VALIDATE --> W_DB W_EVENT --> KAFKA KAFKA --> R_BAL_PROJ KAFKA --> R_HIST_PROJ R_BAL_PROJ --> R_CACHE R_BAL_PROJ --> R_BAL_DB R_HIST_PROJ --> R_HIST_DB R_BAL_CLIENT --> R_CACHE R_BAL_CLIENT -.->|Cache miss| R_BAL_DB R_HIST_CLIENT --> R_HIST_DB
6.3 Saga Pattern cho Transfer chi tiết
stateDiagram-v2 [*] --> INITIATED: Transfer Request received INITIATED --> VALIDATING: Validate balance & fraud VALIDATING --> DEBITING: Validation passed VALIDATING --> REJECTED: Validation failed<br/>(insufficient balance,<br/>fraud detected) DEBITING --> DEBITED: Debit successful DEBITING --> FAILED: Debit failed<br/>(DB error, timeout) DEBITED --> CREDITING: Proceed to credit CREDITING --> COMPLETED: Credit successful CREDITING --> COMPENSATING: Credit failed<br/>(wallet frozen,<br/>DB error) COMPENSATING --> COMPENSATED: Debit reversed<br/>successfully COMPENSATING --> STUCK: Compensation failed<br/>→ Dead Letter Queue COMPLETED --> [*] COMPENSATED --> [*] REJECTED --> [*] FAILED --> [*] STUCK --> MANUAL_REVIEW: Engineer investigates MANUAL_REVIEW --> [*]
6.4 Reconciliation Pipeline chi tiết
flowchart TB subgraph "Scheduled Trigger" CRON["Cron Job<br/>Every 1 hour"] end subgraph "Data Collection" EVT_COUNT["Event Store<br/>Count events<br/>per time window"] LEDGER_COUNT["Ledger DB<br/>Count entries<br/>per time window"] BAL_SUM["Balance View<br/>Sum all balances"] EXPECTED["Expected Total<br/>(Initial deposits<br/>minus withdrawals)"] end subgraph "Comparison Rules" RULE1["Rule 1:<br/>Event count = Ledger entry count?"] RULE2["Rule 2:<br/>Sum(all balances) = Expected total?"] RULE3["Rule 3:<br/>For each transfer:<br/>debit amount = credit amount?"] RULE4["Rule 4:<br/>No negative balances?"] end subgraph "Results" MATCH["ALL MATCH<br/>System consistent"] DISC["DISCREPANCY<br/>Mismatch found"] end subgraph "Actions" LOG["Log result<br/>to audit DB"] ALERT["P0 Alert:<br/>PagerDuty + Slack"] REPORT["Generate<br/>discrepancy report"] FREEZE["Optional:<br/>Freeze affected wallets"] end CRON --> EVT_COUNT CRON --> LEDGER_COUNT CRON --> BAL_SUM CRON --> EXPECTED EVT_COUNT --> RULE1 LEDGER_COUNT --> RULE1 BAL_SUM --> RULE2 EXPECTED --> RULE2 EVT_COUNT --> RULE3 LEDGER_COUNT --> RULE3 BAL_SUM --> RULE4 RULE1 --> MATCH RULE2 --> MATCH RULE3 --> MATCH RULE4 --> MATCH RULE1 --> DISC RULE2 --> DISC RULE3 --> DISC RULE4 --> DISC MATCH --> LOG DISC --> ALERT DISC --> REPORT DISC --> FREEZE
7. Aha Moments & Pitfalls
7.1 Aha Moments — Những khoảnh khắc “à há”
| # | Insight | Giải thích |
|---|---|---|
| 1 | Event sourcing làm audit trở nên trivial | Không cần xây audit system riêng. Event log chính là audit trail. Mọi thay đổi đã được ghi lại tự động, không thể bị xóa hoặc sửa. |
| 2 | CQRS cho phép scale read và write độc lập | Balance inquiry (read) nhiều gấp 10-100x so với transfer (write). CQRS cho phép thêm Redis replica, read DB replica mà không ảnh hưởng write path. |
| 3 | Exactly-once không phải về delivery, mà về idempotency | Không thể đảm bảo network chỉ gửi message 1 lần. Nhưng có thể đảm bảo server chỉ xử lý message 1 lần bằng idempotency key. At-least-once delivery + idempotent processing = effectively exactly-once. |
| 4 | Event store có thể rebuild mọi thứ | Materialized view bị hỏng? Xóa và rebuild. Thêm báo cáo mới? Replay events. Migrate schema? Replay events với logic mới. Source of truth không bao giờ mất. |
| 5 | Saga là “distributed ACID” với trade-offs | Saga không cho atomicity như single DB transaction. Nó cho eventual consistency với compensating actions. Nhưng đó là cái giá phải trả để có scalability và availability. |
| 6 | Snapshot là chìa khóa để event sourcing hoạt động trong thực tế | Event sourcing lý thuyết thì đẹp, nhưng replay 10 triệu events để lấy balance là không khả thi. Snapshot + partial replay làm cho nó thực tế được. |
| 7 | Consistency level phải khác nhau cho các use case khác nhau | Balance hiển thị cho user: eventually consistent (từ cache) là OK. Balance check trước khi debit: bắt buộc strong consistency (từ database). Không phải mọi thứ đều cần strong consistency. |
| 8 | Dead Letter Queue là mạng an toàn | Trong hệ thống tài chính, không bao giờ để giao dịch “biến mất”. DLQ đảm bảo mọi giao dịch thất bại đều được ghi nhận và xử lý. |
7.2 Pitfalls — Những cái bẫy thường gặp
| # | Pitfall | Tại sao nguy hiểm | Cách tránh |
|---|---|---|---|
| 1 | Đọc balance từ cache để kiểm tra trước khi debit | Cache có thể stale. User A có 100K trong DB nhưng cache hiển thị 200K (chưa cập nhật). Nếu debit 150K dựa trên cache → số dư âm. | Luôn đọc từ database (strong consistency) khi kiểm tra balance cho debit. Cache chỉ dùng cho display. |
| 2 | Không có idempotency key | Client retry → duplicate transfer → user mất tiền gấp đôi | Bắt buộc idempotency key cho mỗi transfer request. |
| 3 | Dùng 2PC cho cross-shard transfer | Coordinator failure → giao dịch bị block → lock contention → throughput giảm | Dùng Saga pattern thay vì 2PC. |
| 4 | Không có compensating action | Credit fail sau khi debit thành công → user A mất tiền vĩnh viễn | Mỗi saga step phải có compensating action tương ứng. |
| 5 | Replay events không theo thứ tự | Events bị xáo trộn → balance tính sai | Đảm bảo strict ordering trong event store. Dùng sequence number, không chỉ dựa vào timestamp. |
| 6 | Không có reconciliation | Bug im lặng tạo ra money discrepancy, chỉ phát hiện khi quá trễ | Reconciliation định kỳ (hourly, daily). Bất kỳ mismatch nào đều phải là P0 alert. |
| 7 | Event store không immutable | Ai đó UPDATE hoặc DELETE event → mất audit trail, mất source of truth | Event store phải append-only. Dùng database permissions để ngăn UPDATE/DELETE. |
| 8 | Không có DLQ | Giao dịch fail → mất tích → user không biết chuyện gì đã xảy ra | Mọi giao dịch fail phải vào DLQ. Không bao giờ ignore failure. |
| 9 | Snapshot không consistent với event store | Snapshot bị lỗi → rebuild balance sai | Snapshot phải bao gồm event sequence number. Khi rebuild, chỉ replay events sau sequence number của snapshot. |
| 10 | Eventually consistent là OK cho mọi thứ | Balance display eventually consistent là OK. Nhưng balance check cho debit phải strong consistent. Nhầm lẫn 2 cái này → money loss. | Phân biệt rõ: display (eventual OK) vs business logic (strong required). |
7.3 Interview Tips
| Tip | Giải thích |
|---|---|
| Bắt đầu từ single DB solution | Cho thấy bạn hiểu vấn đề cần giải quyết trước khi nhảy vào giải pháp phức tạp |
| Giải thích tại sao cần event sourcing | Không phải vì “nó cool”, mà vì nó giải quyết vấn đề cụ thể: audit trail, rebuild state, distributed consistency |
| Vẽ state machine cho saga | Interviewer thích thấy bạn suy nghĩ có cấu trúc về trạng thái và chuyển trạng thái |
| Nói về trade-offs | Event sourcing có nhược điểm (storage, complexity, eventual consistency). Phải biết và nói rõ |
| Mention reconciliation | Rất ít ứng viên nghĩ đến reconciliation. Đây là điểm khác biệt giữa “thiết kế trên giấy” và “thiết kế cho production” |
8. Tổng kết — Mental Model
8.1 Digital Wallet = Event Sourcing + CQRS + Saga
┌─────────────────────────────────────────────────────────┐
│ DIGITAL WALLET │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Event Sourcing │ │ CQRS │ │
│ │ • Source of │ │ • Write: events │ │
│ │ truth = events│ │ • Read: views │ │
│ │ • Immutable log │ │ • Scale doc lap │ │
│ │ • Rebuild state │ │ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Saga Pattern │ │ Exactly-once │ │
│ │ • Distributed │ │ • Idempotency │ │
│ │ transaction │ │ key │ │
│ │ • Compensating │ │ • Event dedup │ │
│ │ actions │ │ • At-least-once │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Reconciliation │ │ Audit Trail │ │
│ │ • Hourly/daily │ │ • Free with │ │
│ │ • Event vs │ │ event sourcing│ │
│ │ ledger vs │ │ • Immutable │ │
│ │ balance │ │ • Complete │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
8.2 Khi nào dùng giải pháp nào?
| Scale | Giải pháp | Khi nào |
|---|---|---|
| Startup (< 1K TPS) | Single PostgreSQL với ACID transaction | Khi bắt đầu, đơn giản, dễ hiểu, dễ maintain |
| Growth (1K-10K TPS) | PostgreSQL + Event sourcing + CQRS | Khi cần audit trail, khi read/write ratio không cân bằng |
| Scale (10K-100K TPS) | Sharded event store + Saga + CQRS + Redis cache | Khi single DB không đủ, cần distributed transaction |
| Massive (100K+ TPS) | Full architecture như trên + multi-region + dedicated event store | Alipay, WeChat Pay level |
Lời khuyên cuối cùng: Hãy nhớ rằng đơn giản là tốt nhất. Bắt đầu với single database transaction. Chỉ thêm event sourcing, CQRS, saga khi thực sự cần. Premature optimization là root of all evil — đặc biệt trong fintech, nơi mà correctness quan trọng hơn performance.
9. Internal Links
| Link | Lý do liên quan |
|---|---|
| Case-Design-Payment-System | Payment system là “anh em” với digital wallet — cùng domain fintech, nhưng focus khác (external PSP vs internal wallet) |
| Tuan-11-Microservices-Pattern | Saga pattern, event-driven architecture, service communication — tất cả là microservice patterns |
| Tuan-08-Message-Queue | Kafka là backbone của event distribution trong CQRS. Hiểu message queue để hiểu event streaming |
| Tuan-15-Data-Security-Encryption | Encryption, key management, PCI-DSS — bảo mật là bắt buộc cho hệ thống tài chính |
| Tuan-14-AuthN-AuthZ-Security | 2FA, session auth, device binding — authentication là tuyến phòng thủ đầu tiên |
| Tuan-07-Database-Sharding-Replication | Sharding wallet data, replication cho durability — hiểu database scaling để hiểu tại sao cần distributed transaction |
| Tuan-02-Back-of-the-envelope | Estimation section sử dụng kỹ thuật từ tuần này |
| Tuan-13-Monitoring-Observability | Monitoring, alerting, reconciliation — DevOps section xây dựng trên kiến thức observability |