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ạnhPayment System (Chapter 7)Digital Wallet (Chapter 12)
FocusXử 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 dependencyPhụ thuộc card network, bankChủ yếu nội bộ — kiểm soát toàn bộ
Tính chất tiềnTiền “chạy qua” hệ thốngTiền “nằm trong” hệ thống
ConsistencyPSP đảm bảo phần lớnTa phải tự đảm bảo — đây là thách thức lớn nhất
Core patternIdempotency + webhook + reconciliationEvent sourcing + CQRS + Saga
AuditLog-basedEvent log là source of truth
Ví dụShopee xử lý thanh toán qua VNPayMoMo 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ý doGiải thích
Mọi super app đều có walletGrab, Gojek, MoMo, ZaloPay, WeChat Pay — wallet là core feature
Event sourcing là pattern phổ biếnKhông chỉ fintech — banking, gaming, e-commerce đều dùng
CQRS là kiến trúc quan trọngTách read và write — pattern cần thiết cho hệ thống lớn
Saga pattern cho distributed transactionKhông chỉ wallet — bất kỳ hệ thống microservice nào cũng cần
Interview favoriteDigital 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ốngQuy môĐặc điểm
Alipay1.3 tỷ người dùng, 100,000+ TPS peak (Singles’ Day)Lớn nhất thế giới
WeChat Pay900 triệu người dùngTích hợp siêu app
MoMo35+ triệu người dùng tại Việt NamVí điện tử số 1 VN
GrabPay180+ triệu người dùng Đông Nam ÁSuper app wallet
PayPal430+ triệu tài khoảnWallet + 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ỏiTrả lờiGhi 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?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ôngBalance >= 0 luôn luôn
Transaction history?User xem lịch sử giao dịch
Real-time balance?User thấy số dư ngay sau giao dịch

2.1.2 Functional Requirements

IDChức năngMô tả chi tiết
FR1Transfer moneyChuyển tiền từ wallet A sang wallet B, đảm bảo A giảm đúng X và B tăng đúng X
FR2Balance inquiryUser xem số dư hiện tại của ví — phải chính xác và real-time
FR3Transaction historyUser xem danh sách các giao dịch đã thực hiện (gửi, nhận, thất bại)
FR4Idempotent transferCùng một request gửi nhiều lần chỉ được xử lý 1 lần
FR5Balance validationKhông cho chuyển tiền nếu số dư không đủ

2.1.3 Non-Functional Requirements

Yêu cầuMục tiêuLý do
CorrectnessZero money loss/creationTiền mất = kiện tụng, tiền tạo ra = gian lận
Exactly-onceMỗi transfer chỉ thực hiện 1 lầnDuplicate = mất tiền hoặc tạo tiền
ConsistencyStrong consistency cho balance updateSố dư phải chính xác tại mọi thời điểm
Availability99.99% uptime (~52 phút downtime/năm)Wallet không hoạt động = user không thể chi tiêu
DurabilityZero data lossMọi giao dịch phải được lưu vĩnh viễn
AuditabilityMọi thay đổi có audit trailCompliance và dispute resolution
LatencyP99 < 500ms cho transferUser experience
Throughput1M transactions/day, peak 50 TPSScale 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

ComponentVai tròAnalogy
Wallet ServiceQuản lý ví, kiểm tra số dư, thực hiện chuyển tiềnThủ ngân ngân hàng
Transaction ServiceĐiều phối toàn bộ flow chuyển tiền, đảm bảo atomicityGiám đốc chi nhánh
Ledger ServiceGhi nhận mọi giao dịch theo chuẩn double-entrySổ cái kế toán
Audit ServiceKiểm tra tính nhất quán, đối soát, phát hiện bất thườngKiểm toán viên
Event StoreLưu trữ mọi sự kiện (source of truth)Cuốn nhật ký bất tử
Balance CacheCache số dư để đọc nhanhBả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íchVí dụ
Race condition2 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 crashServer restart = mất hết số dưMoMo crash → 35 triệu người dùng mất hết tiền
Không distributedMột server duy nhất = single point of failureServer đổ = toàn bộ hệ thống chết
Không có audit trailChỉ lưu state, không lưu lịch sử thay đổiKhông thể điều tra khi có tranh chấp
Không có durabilityMemory là volatile — mất điện = mất dataKhô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:

  1. BEGIN TRANSACTION
  2. Kiểm tra số dư A >= X
  3. UPDATE wallet SET balance = balance - X WHERE user_id = A
  4. UPDATE wallet SET balance = balance + X WHERE user_id = B
  5. INSERT INTO ledger (debit A, credit B, amount X)
  6. COMMIT

Tại sao hoạt động tốt (cho single database):

Đặc điểm ACIDÁp dụng cho walletKết quả
AtomicityCả debit và credit hoặc cả hai thành công, hoặc cả hai rollbackKhông bao giờ A mất tiền mà B không nhận
ConsistencyConstraint: balance >= 0Không thể trừ nhiều hơn số dư
Isolation2 giao dịch đồng thời không xung độtKhông race condition
DurabilityData được lưu xuống disk, có WALKhông mất data khi crash

Giới hạn của database-based solution:

Giới hạnGiải thíchKhi nào gặp
Single database bottleneckToàn bộ giao dịch đi qua 1 DBKhi scale quá 10K TPS
Vertical scaling limitKhông thể tăng mãi CPU/RAM của 1 serverKhi đạt giới hạn phần cứng
Không distributedA và B phải nằm trên cùng 1 DBKhi cần shard data theo user
Lock contentionNhiề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 impossibleTransaction không thể span across regionsKhi 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:

PhaseHành độngGiải thích
Phase 1: PrepareCoordinator 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: CommitNếu tất cả “sẵn sàng” → Coordinator nói: “Commit!”Cả 2 shard commit đồng thời
RollbackNế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íchHậu quả
Coordinator failureCoordinator crash sau Phase 1, trước Phase 2Các participant “treo” — không biết commit hay rollback
BlockingParticipant phải giữ lock cho đến khi nhận lệnh từ coordinatorCác giao dịch khác bị block, throughput giảm mạnh
Latency cao2 round-trip giữa coordinator và tất cả participantLatency tăng gấp đôi so với single DB
Không fault-tolerantMột participant crash = toàn bộ transaction bị blockKhông phù hợp cho hệ thống high-availability
Network partitionCoordinator và participant không liên lạc đượcTrạ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=0
    • CreditCompleted: user=A, amount=500,000, source=bank_deposit
    • DebitCompleted: 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ánhState-basedEvent Sourcing
Lưu gìTrạng thái hiện tạiMọi sự kiện đã xảy ra
UpdateGhi đè 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
AuditCần thêm audit log riêngEvent log chính là audit trail
DebugKhó — chỉ thấy state hiện tạiDễ — replay events để thấy chính xác chuyện gì đã xảy ra
RebuildKhông thểCó thể rebuild state bất kỳ lúc nào
StorageÍt hơnNhiề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:

EventMô tảData
TransferInitiatedGiao dịch được tạotransfer_id, from_wallet, to_wallet, amount, timestamp, idempotency_key
BalanceCheckedKiểm tra số dư thành côngtransfer_id, wallet_id, current_balance, required_amount
DebitCompletedĐã trừ tiền từ wallet Atransfer_id, wallet_id, amount, balance_after
CreditCompletedĐã cộng tiền vào wallet Btransfer_id, wallet_id, amount, balance_after
TransferCompletedGiao dịch hoàn tấttransfer_id, status=SUCCESS, timestamp
TransferFailedGiao dịch thất bạitransfer_id, status=FAILED, reason, timestamp
DebitReversedHoà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ầnVai trò
Event StoreSource of truth — lưu toàn bộ events
Materialized ViewDerived data — balance được tính từ events
Event ProcessorConsumer đọ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ốngKhông có idempotencyCó idempotency
Client gửi 1 lầnOKOK
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ểmGiải thíchTại sao quan trọng
Append-onlyChỉ thêm event mới, không bao giờ update hay deleteĐảm bảo immutability — audit trail không thể bị sửa
OrderedEvents được sắp xếp theo thời gian (hoặc sequence number)Replay phải theo đúng thứ tự
PersistentLưu trên disk với replicationKhông mất data
QueryableCó thể query events theo wallet_id, transfer_id, time rangeHỗ trợ transaction history và audit

Lựa chọn công nghệ cho Event Store:

Công nghệƯu điểmNhược điểmKhi nào dùng
KafkaThroughput cao, distributed, matureKhó query theo criteria phức tạp, retention có limitEvent streaming giữa services
EventStoreDBThiết kế dành riêng cho event sourcing, projections, subscriptionsÍt mature hơn, community nhỏ hơnEvent sourcing là core của hệ thống
PostgreSQL (append-only table)Quen thuộc, SQL query, ACIDThroughput thấp hơn Kafka, phải tự implement event sourcing logicHệ thống nhỏ-vừa, team quen PostgreSQL
DynamoDB (append-only)Serverless, auto-scaling, high throughputVendor 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 nhauWrite cần consistency cao (ACID). Read cần throughput cao (1000x nhiều hơn write)
Event store không tối ưu cho readReplay 10,000 events để lấy balance = chậm. Cần materialized view
Scale độc lậpWrite ít nhưng quan trọng. Read nhiều nhưng có thể chấp nhận eventually consistent
Model khác nhauWrite 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):

  1. Nhận transfer command
  2. Validate (balance check, fraud check)
  3. Ghi events vào Event Store (source of truth)
  4. Publish events lên Kafka

Query side (Read path):

  1. Kafka consumer (projector) đọc events
  2. Cập nhật materialized views (balance table, history table)
  3. Cập nhật Redis cache
  4. User query đọc từ materialized view hoặc cache
Đặc điểmCommand SideQuery Side
Data modelEvents (append-only)Materialized views (tables, cache)
ConsistencyStrong (ACID)Eventually consistent
ThroughputThấp hơn (cần lock, validate)Cao hơn (read-only, có cache)
ScaleKhó scale (consistency requirement)Dễ scale (thêm read replica, cache)
LatencyCao 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ạnhGiả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 rebuildLoad 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 ở đâuCùng event store hoặc object storage (S3)

Snapshot strategy:

StrategyTần suấtƯu điểmNhược điểm
Every N eventsMỗi 10,000 eventsDự đoán được rebuild timeHot wallet có snapshot nhiều, cold wallet ít
Time-basedMỗi 1 giờĐơn giảnKhông tối ưu cho wallet có nhiều events
On-demandKhi rebuild cầnÍt tốn storagePhải đợi lâu khi rebuild
HybridMỗi 10,000 events HOẶC mỗi 6 giờ (cái nào đến trước)Cân bằngPhứ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ểmOrchestrationChoreography
Cách hoạt độngMột “orchestrator” điều phối từng stepMỗi service tự quyết định hành động tiếp theo dựa trên event
Control flowTập trung tại orchestratorPhân tán giữa các service
Dễ hiểuCó — đọc orchestrator là hiểu toàn bộ flowKhó — phải xem nhiều service để hiểu flow
Dễ debugCó — log tập trungKhó — log phân tán
CouplingOrchestrator biết tất cả servicesServices chỉ biết events, không biết nhau
Single point of failureOrchestrator là SPOFKhông có SPOF
Dùng cho walletNên dùng — flow phải chính xác, dễ auditPhù 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ì:

  1. Transfer flow có thứ tự nghiêm ngặt (debit trước, credit sau)
  2. Cần biết chính xác trạng thái của mỗi step
  3. Compensating action phải được điều phối chính xác
  4. 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:

StateMô tảChuyển trạng thái
INITIATEDTransfer 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)
COMPLETEDGiao 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)
FAILEDThấ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ầnCách hoạt độngGiải quyết vấn đề gì
Idempotency keyClient 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 deduplicationMỗi event có unique event_id. Consumer check trước khi processEvent được deliver nhiều lần nhưng chỉ process 1 lần
At-least-once deliveryKafka đả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ó idempotencyNếu có idempotency
Lần 1: Trừ A 100K, Cộng B 100KLần 1: Trừ A 100K, Cộng B 100K
Lần 2: Trừ A 100K nữa, Cộng B 100K nữaLần 2: Skip, trả về kết quả cũ
Tổng: A mất 200K, B nhận 200K — SAITổ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ý
ScenarioGiải thíchXử lý
Debit thành công, credit timeoutA đã bị trừ, nhưng không biết B đã nhận chưaCheck trạng thái credit → nếu thất bại → compensate (hoàn tiền A)
Debit thành công, credit service downA đã bị trừ, credit service không phản hồiRetry với exponential backoff → nếu quá max retry → compensate
Orchestrator crash giữa chừngĐang xử lý thì orchestrator dieOrchestrator mới khởi động → đọc saga state → tiếp tục từ step cuối
Database crashDatabase không ghi được eventRetry → nếu vẫn fail → reject transfer
Kafka consumer lagConsumer xử lý chậm, balance view bị staleAlert → scale consumer → user thấy balance cũ nhưng vẫn đúng
Network partition2 shard không liên lạc đượcSaga bị “treo” → timeout → compensate → retry sau
3.7.2 Retry Strategy
AspectChi tiết
Retry policyExponential backoff: 1s → 2s → 4s → 8s → 16s
Max retries5 lần (tổng ~31 giây)
JitterThêm random delay để tránh “thundering herd”
IdempotentMỗi retry gửi cùng idempotency_key → không gây duplicate
Timeout per step5 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ướcHành động
1Transfer bị đẩy vào Dead Letter Queue
2Alert team (PagerDuty, Slack)
3Engineer điều tra nguyên nhân
4Fix lỗi
5Replay 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ý doVí dụ
Dispute resolutionUser A nói “Tôi không chuyển tiền này!” → Kiểm tra event log
Regulatory complianceNgân hàng Nhà nước yêu cầu kiểm tra giao dịch → Có sẵn
Fraud detectionPhát hiện pattern bất thường từ event log
Bug investigationTì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 entrycredit entry với tổng bằng 0.

Giao dịchDebit EntryCredit EntryTổng
A → B, 100KWallet A: -100,000Wallet B: +100,0000
C → D, 50KWallet C: -50,000Wallet D: +50,0000

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?
MetricKhông có cacheCó Redis cache
Balance query latency10-50ms (database read)1-2ms (Redis read)
Database loadCao (mỗi balance check = 1 DB query)Thấp (chỉ khi cache miss)
Scale readKhó (database bottleneck)Dễ (thêm Redis replica)
3.9.2 Cache Invalidation Strategy
StrategyMô tảDùng khi
Write-throughCập nhật cache ngay khi ghi eventBalance phải chính xác ngay sau transfer
Event-drivenKafka consumer cập nhật cache khi nhận eventChấp nhận delay vài ms
TTL-basedCache hết hạn sau N giây, đọc lại từ DBBalance hiển thị (không cần 100% real-time)

Chiến lược khuyến nghị: Write-through + TTL backup

  1. Khi DebitCompleted/CreditCompleted → cập nhật Redis ngay lập tức
  2. Đặt TTL 60 giây cho mỗi cache entry → nếu write-through fail, cache sẽ expire và đọc lại từ DB
  3. 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 caseVí 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 reportingBáo cáo số dư cuối ngày/cuối tháng cho ngân hàng nhà nước
AuditKiểm tra số dư tại bất kỳ thời điểm nào
3.10.2 Cách implement
CáchMô tảƯu điểmNhược điểm
Event replayReplay events từ đầu đến thời điểm cầnChính xác tuyệt đốiChậm với nhiều events
Snapshot + replayLoad snapshot gần nhất trước thời điểm, replay events còn lạiNhanh hơnCần lưu nhiều snapshots
Temporal tableDatabase lưu mỗi phiên bản của row (valid_from, valid_to)Query nhanhStorage lớn, schema phức tạp
Daily snapshotLưu số dư cuối mỗi ngàyĐơn giản, query nhanhChỉ 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ầngBiện phápGiải thích
Data at restAES-256 encryption cho database và event storeDù ai truy cập disk cũng không đọc được data
Data in transitTLS 1.3 cho mọi giao tiếp giữa servicesKhông thể nghe lén network traffic
Field-level encryptionEncrypt trường nhạy cảm (balance, account_id) riêngDù database bị compromise, data vẫn an toàn
Key managementAWS KMS hoặc HashiCorp VaultKhông lưu key trong code hoặc config file
Encryption key rotationThay 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ậtMô tảVí dụ
Velocity checkGiới hạn số giao dịch trong khoảng thời gianTối đa 10 giao dịch/phút, tối đa 50 triệu/ngày
Unusual pattern detectionPhá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-anomalyGiao dịch từ địa điểm bất thườngĐang ở Hà Nội mà có giao dịch từ Cambodia
Device fingerprintingGiao dịch từ thiết bị lạGiao dịch từ thiết bị mới chưa bao giờ dùng
Graph analysisPhá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 scoringChấm điểm rủi ro cho mỗi giao dịchScore > 0.8 → block và yêu cầu xác minh

4.3 AML (Anti-Money Laundering) Compliance

Yêu cầuMô tả
KYC (Know Your Customer)Xác minh danh tính trước khi cho phép giao dịch lớn
Transaction monitoringTheo 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 screeningKiểm tra danh sách đen quốc tế (OFAC, UN)
Record keepingLưu trữ giao dịch tối thiểu 5-7 năm

4.4 Authentication cho Transfers

BướcMô tảTại sao
Session authJWT token cho mỗi requestXác nhận danh tính user
2FA cho transfer lớnOTP qua SMS/app cho giao dịch trên ngưỡng (5 triệu VND)Ngăn chặn unauthorized transfer
PIN/BiometricYêu cầu PIN hoặc vân tay trước mỗi transferBảo vệ ngay cả khi điện thoại bị mất
Device bindingRà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-DSSMô tả
Không lưu card number trên serverDùng tokenization (thay card number bằng token)
Network segmentationTách mạng xử lý thẻ riêng
Penetration testingTest bảo mật định kỳ (hàng quý)
Access controlChỉ người được ủy quyền mới truy cập cardholder data
Logging & monitoringGhi 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-EncryptionTuan-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

MetricMô tảThreshold cảnh báo
Transaction success rateTỷ lệ giao dịch thành công< 99.9% → P1 alert
Transaction latency (P50, P99)Thời gian xử lý giao dịchP99 > 500ms → investigate
Event processing lagĐộ trễ giữa ghi event và cập nhật materialized view> 5 giây → scale consumer
Balance consistency checkKết quả reconciliation định kỳAny mismatch → P0 alert (critical)
Dead letter queue depthSố giao dịch bị stuck> 0 trong 5 phút → investigate
Kafka consumer lagSố message chưa được xử lý> 10,000 → scale consumer
Snapshot freshnessThời gian từ snapshot gần nhất> 24h → tạo snapshot mới
Cache hit rateTỷ lệ balance query hit cache< 90% → kiểm tra cache strategy
Error rate by typePhân loại lỗi: timeout, insufficient balance, system errorSystem error > 0.1% → investigate

5.2 Reconciliation Schedule

LoạiTần suấtMô tả
Real-timeMỗi giao dịchKiểm tra debit + credit = 0 cho mỗi transfer
Micro-batchMỗi 5 phútSo sánh event count với ledger entry count
HourlyMỗi giờTổng balance tất cả wallets = tổng tiền trong hệ thống
DailyMỗi ngày 01:00Full reconciliation: event store vs ledger vs balance view
MonthlyCuối thángBáo cáo cho compliance và kế toán

5.3 Alerting Hierarchy

LevelTình huốngHành động
P0 (Critical)Balance mismatch phát hiện bởi reconciliationTấ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âyEngineer điều tra trong 1 giờ
P3 (Low)Cache hit rate giảmXem xét trong sprint tiếp theo

5.4 Disaster Recovery

ScenarioRTORPOStrategy
Single server failure< 1 phút0 (no data loss)Auto-failover (database replica, service restart)
Database failure< 5 phút0Synchronous replication + auto-failover
Datacenter failure< 30 phút< 1 giâyCross-region replication, DNS failover
Event store corruption< 2 giờ0Backup + replay từ backup

5.5 Deployment Strategy

StrategyMô tảÁp dụng cho wallet
Blue-green deployment2 môi trường giống hệt, chuyển traffic khi sẵn sàngCho stateless services (API gateway, fraud detection)
Canary releaseDeploy cho 1-5% traffic trước, rồi mở rộngCho wallet service (cần kiểm tra consistency trước khi rollout)
Feature flagBật/tắt feature không cần deploy lạiCho 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á”

#InsightGiải thích
1Event sourcing làm audit trở nên trivialKhô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.
2CQRS cho phép scale read và write độc lậpBalance 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.
3Exactly-once không phải về delivery, mà về idempotencyKhô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.
4Event 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.
5Saga là “distributed ACID” với trade-offsSaga 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ó scalabilityavailability.
6Snapshot 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.
7Consistency level phải khác nhau cho các use case khác nhauBalance 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.
8Dead Letter Queue là mạng an toànTrong 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

#PitfallTại sao nguy hiểmCách tránh
1Đọc balance từ cache để kiểm tra trước khi debitCache 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.
2Không có idempotency keyClient retry → duplicate transfer → user mất tiền gấp đôiBắt buộc idempotency key cho mỗi transfer request.
3Dùng 2PC cho cross-shard transferCoordinator failure → giao dịch bị block → lock contention → throughput giảmDùng Saga pattern thay vì 2PC.
4Không có compensating actionCredit fail sau khi debit thành công → user A mất tiền vĩnh viễnMỗi saga step phải có compensating action tương ứng.
5Replay 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.
6Không có reconciliationBug 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.
7Event store không immutableAi đó UPDATE hoặc DELETE event → mất audit trail, mất source of truthEvent store phải append-only. Dùng database permissions để ngăn UPDATE/DELETE.
8Không có DLQGiao dịch fail → mất tích → user không biết chuyện gì đã xảy raMọi giao dịch fail phải vào DLQ. Không bao giờ ignore failure.
9Snapshot không consistent với event storeSnapshot bị lỗi → rebuild balance saiSnapshot phải bao gồm event sequence number. Khi rebuild, chỉ replay events sau sequence number của snapshot.
10Eventually 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

TipGiải thích
Bắt đầu từ single DB solutionCho 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 sourcingKhô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 sagaInterviewer 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-offsEvent sourcing có nhược điểm (storage, complexity, eventual consistency). Phải biết và nói rõ
Mention reconciliationRấ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?

ScaleGiải phápKhi nào
Startup (< 1K TPS)Single PostgreSQL với ACID transactionKhi bắt đầu, đơn giản, dễ hiểu, dễ maintain
Growth (1K-10K TPS)PostgreSQL + Event sourcing + CQRSKhi cần audit trail, khi read/write ratio không cân bằng
Scale (10K-100K TPS)Sharded event store + Saga + CQRS + Redis cacheKhi single DB không đủ, cần distributed transaction
Massive (100K+ TPS)Full architecture như trên + multi-region + dedicated event storeAlipay, 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.


LinkLý do liên quan
Case-Design-Payment-SystemPayment 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-PatternSaga pattern, event-driven architecture, service communication — tất cả là microservice patterns
Tuan-08-Message-QueueKafka là backbone của event distribution trong CQRS. Hiểu message queue để hiểu event streaming
Tuan-15-Data-Security-EncryptionEncryption, 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-Security2FA, session auth, device binding — authentication là tuyến phòng thủ đầu tiên
Tuan-07-Database-Sharding-ReplicationSharding wallet data, replication cho durability — hiểu database scaling để hiểu tại sao cần distributed transaction
Tuan-02-Back-of-the-envelopeEstimation section sử dụng kỹ thuật từ tuần này
Tuan-13-Monitoring-ObservabilityMonitoring, alerting, reconciliation — DevOps section xây dựng trên kiến thức observability