Domain: Fintech / Neobank
Fintech là một trong những domain khắt khe nhất về độ chính xác — sai một đồng là sai, không có "eventual consistency" cho số dư tài khoản. Section này mô tả cách các hệ thống như Cake, MoMo, VNPay, hoặc Revolut thực sự hoạt động bên trong.
1. Core Banking — Ledger và Double-Entry Accounting
1.1 Double-Entry Accounting là gì?
Mọi giao dịch tài chính đều được ghi theo nguyên tắc kế toán kép: mỗi giao dịch tác động lên ít nhất hai tài khoản, tổng nợ (Debit) luôn bằng tổng có (Credit). Đây là nguyên tắc 700 năm tuổi, và vẫn là nền tảng của mọi core banking system.
T-Account cho Alice và Bob:
Alice Bob
┌──────┬──────────┐ ┌──────────┬──────┐
│Debit │ Credit │ │ Debit │Credit│
├──────┼──────────┤ ├──────────┼──────┤
│ │ -100k │ │ +100k │ │
└──────┴──────────┘ └──────────┴──────┘
Alice chuyển 100k cho Bob:
→ Alice Account: Credit 100k (giảm số dư)
→ Bob Account: Debit 100k (tăng số dư)
→ Tổng net = 0 → Ledger luôn balanced
Tại sao double-entry quan trọng với engineer?
Nó không chỉ là accounting convention — nó là cơ chế phát hiện lỗi tự nhiên. Nếu tổng ledger không bằng 0, có bug hoặc có gian lận. Đây là invariant mà mọi payment system phải bảo đảm.
1.2 Ledger Schema thực tế
-- Immutable ledger entries — không bao giờ UPDATE hay DELETE
CREATE TABLE ledger_entries (
id BIGSERIAL PRIMARY KEY,
entry_id UUID NOT NULL UNIQUE, -- idempotency key
account_id UUID NOT NULL,
amount BIGINT NOT NULL, -- tính bằng đơn vị nhỏ nhất (đồng)
direction VARCHAR(6) NOT NULL, -- 'DEBIT' hoặc 'CREDIT'
currency CHAR(3) NOT NULL DEFAULT 'VND',
tx_id UUID NOT NULL, -- nhóm entries cùng transaction
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- KHÔNG có updated_at — ledger entries là immutable
CONSTRAINT chk_direction CHECK (direction IN ('DEBIT', 'CREDIT')),
CONSTRAINT chk_amount CHECK (amount > 0)
);
-- Account balance là view computed từ ledger, không phải stored value
CREATE VIEW account_balances AS
SELECT
account_id,
SUM(CASE WHEN direction = 'CREDIT' THEN amount
WHEN direction = 'DEBIT' THEN -amount END) AS balance
FROM ledger_entries
GROUP BY account_id;
Tiền được lưu trữ dưới dạng integer (đồng, xu, cent) — tuyệt đối không dùng FLOAT/DOUBLE vì floating point không biểu diễn chính xác giá trị thập phân. 0.1 + 0.2 = 0.30000000000000004 trong IEEE 754.
1.3 Balance Snapshot Pattern
Tính balance từ toàn bộ ledger history mỗi lần query sẽ chậm khi ledger có hàng tỷ entries. Pattern phổ biến là lưu periodic snapshots:
Ledger Entries (append-only)
─────────────────────────────────────────────────────► time
[entry1][entry2]...[entry_1M] | snapshot @1M | [entry_1M+1]...
Balance query = snapshot_balance + SUM(entries since snapshot)
type BalanceService struct {
db *sql.DB
}
func (s *BalanceService) GetBalance(ctx context.Context, accountID uuid.UUID) (int64, error) {
// Bước 1: lấy snapshot gần nhất
var snapshotBalance int64
var snapshotSeq int64
err := s.db.QueryRowContext(ctx, `
SELECT balance, last_entry_seq
FROM balance_snapshots
WHERE account_id = $1
ORDER BY last_entry_seq DESC
LIMIT 1
`, accountID).Scan(&snapshotBalance, &snapshotSeq)
if err != nil && err != sql.ErrNoRows {
return 0, err
}
// Bước 2: cộng dồn các entries sau snapshot
var delta int64
err = s.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM(
CASE WHEN direction = 'CREDIT' THEN amount
WHEN direction = 'DEBIT' THEN -amount END
), 0)
FROM ledger_entries
WHERE account_id = $1
AND id > $2
`, accountID, snapshotSeq).Scan(&delta)
if err != nil {
return 0, err
}
return snapshotBalance + delta, nil
}
2. Payment Flow: Transfer, Settlement, Clearing
2.1 Internal vs Interbank Transfer
Internal Transfer (cùng ngân hàng):
Alice → Bob (cùng Cake)
─────────────────────────────
Chỉ cần update ledger nội bộ
Tức thì, miễn phí, không cần clearinghouse
Interbank Transfer (khác ngân hàng):
Alice (Cake) → Bob (Vietcombank)
─────────────────────────────────────────────────────
Cake Napas/Citad Vietcombank
│ │ │
│──transfer msg──► │
│ │──forward msg────►│
│ │◄─confirm─────────│
│◄─settlement────│ │
Cake trừ tiền Alice Vietcombank cộng tiền Bob
Napas (National Payment Corporation of Vietnam) là clearinghouse cho Việt Nam, xử lý interbank transfers trong giờ hành chính theo batch. Citad (State Bank of Vietnam) xử lý large-value transfers real-time.
2.2 Settlement vs Clearing
Clearing = Xác nhận nghĩa vụ (ai nợ ai bao nhiêu)
Settlement = Thực hiện thanh toán thực sự (tiền di chuyển)
Ví dụ cuối ngày qua Napas:
Cake → Vietcombank: 500B VND (100,000 giao dịch)
Vietcombank → Cake: 300B VND (80,000 giao dịch)
────────────────────────────────────────────────
Net settlement: Cake thanh toán 200B VND cho Vietcombank
(Không cần chuyển từng giao dịch riêng lẻ)
Đây là "net settlement" — giảm 99% số lượng wire transfers thực sự.
2.3 Reconciliation
Reconciliation là quá trình đối chiếu số liệu giữa hai hệ thống để đảm bảo chúng khớp nhau. Trong fintech, đây là công việc hàng ngày, bắt buộc.
Buổi sáng mỗi ngày:
Cake internal ledger
│
▼ đối chiếu
File settlement từ Napas (T+1)
│
▼ đối chiếu
Statement từ ngân hàng đại lý
Nếu mismatch → tạo "break item" → điều tra:
- Duplicate entry?
- Missing entry?
- Wrong amount?
- Timing difference? (giao dịch cuối ngày cross midnight)
3. Transaction Atomicity — Idempotency trong Payment
3.1 Vấn đề
Client Payment Service Bank API
│ │ │
│── POST /transfer ─────────►│ │
│ │── call bank API ──►│
│ │ │ (timeout!)
│ │◄── timeout ─────────│
│◄── 504 Gateway Timeout ────│ │
│ │ │
│── retry POST /transfer ────►│ │
│ │── call bank API ──►│
│ │◄── success ────────│
│◄── success ────────────────│ │
Câu hỏi: giao dịch đầu tiên có thực sự fail không?
Nếu bank đã xử lý rồi → double charge!
3.2 Idempotency Key Pattern
type TransferRequest struct {
IdempotencyKey string `json:"idempotency_key"` // UUID do client generate
FromAccountID uuid.UUID `json:"from_account_id"`
ToAccountID uuid.UUID `json:"to_account_id"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
}
func (s *PaymentService) Transfer(ctx context.Context, req TransferRequest) (*TransferResult, error) {
// Bước 1: kiểm tra idempotency key đã tồn tại chưa
existing, err := s.store.GetByIdempotencyKey(ctx, req.IdempotencyKey)
if err == nil {
// Đã xử lý trước đó → trả về kết quả cũ (không xử lý lại)
return existing, nil
}
if !errors.Is(err, ErrNotFound) {
return nil, err
}
// Bước 2: xử lý giao dịch mới
result, err := s.executeTransfer(ctx, req)
if err != nil {
return nil, err
}
// Bước 3: lưu kết quả với idempotency key
if saveErr := s.store.SaveResult(ctx, req.IdempotencyKey, result); saveErr != nil {
// Log nhưng không fail — giao dịch đã thành công
s.log.Error("failed to save idempotency record", "err", saveErr)
}
return result, nil
}
Lưu ý quan trọng: Idempotency key phải được lưu trong cùng một DB transaction với ledger entries, hoặc dùng Outbox pattern để đảm bảo tính nhất quán.
3.3 Distributed Transaction trong Payment
Khi payment flow liên quan đến nhiều service (wallet, ledger, notification, limit-check), dùng Saga pattern thay vì 2PC:
Payment Saga (Choreography):
1. PaymentRequested event published
└── LimitService: kiểm tra daily limit
└── LimitChecked event
└── FraudService: real-time scoring
└── FraudCleared event
└── LedgerService: debit/credit
└── PaymentCompleted event
└── NotificationService: push notification
Nếu FraudService reject:
└── FraudRejected event
└── LimitService: release reserved limit (compensate)
└── PaymentFailed event → notify user
4. KYC / AML System Architecture
4.1 KYC — Know Your Customer
User Registration Flow:
User App
│
├── Step 1: Nhập thông tin cá nhân (tên, CCCD, ngày sinh)
├── Step 2: Upload ảnh CCCD (mặt trước + mặt sau)
├── Step 3: Selfie / liveness check
└── Step 4: Ký tên số (eKYC)
Backend:
│
├── OCR Service: extract thông tin từ CCCD
│ (vendor: VinAI, Tessera, hoặc tự build với PaddleOCR)
│
├── Face Match: so khớp ảnh selfie với ảnh CCCD
│ (liveness detection: nhắm mắt, quay đầu → chống ảnh giả)
│
├── ID Verification: kiểm tra CCCD hợp lệ
│ (gọi API Ministry of Public Security hoặc VNeID)
│
└── Risk Scoring: PEP check, sanctions list
(FATF, OFAC, UN sanctions)
4.2 AML — Anti-Money Laundering
AML là yêu cầu pháp lý, không phải tuỳ chọn. Ngân hàng vi phạm AML bị phạt rất nặng.
AML Rule Engine:
Mỗi giao dịch đi qua rule evaluation:
Rule 1: Structuring Detection
IF SUM(transactions, last 24h) > 100M VND
AND MAX(single_tx) < 10M VND
→ Flag: possible structuring (tránh ngưỡng báo cáo)
Rule 2: Velocity Check
IF COUNT(transactions, last 1h) > 20
→ Flag: unusual velocity
Rule 3: Geography Risk
IF recipient_country IN high_risk_countries
AND amount > 5M VND
→ Flag: high-risk cross-border
Rule 4: Dormant Account Activation
IF account_dormant_days > 180
AND transaction_amount > 50M VND
→ Flag: dormant account sudden activity
Output → SAR (Suspicious Activity Report) → Compliance team
4.3 AML Architecture
Transaction Stream
│
▼
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Rule Engine │───►│ Case Manager │───►│ Compliance Portal│
│ (Drools / │ │ (human │ │ (analyst UI) │
│ custom) │ │ review) │ └──────────────────┘
└─────────────┘ └──────────────┘
│
▼
┌─────────────┐
│ ML Model │ (graph-based: phát hiện network của tài khoản liên quan)
│ (GNN) │
└─────────────┘
│
▼
Regulatory Reporting (gửi báo cáo lên NHNN định kỳ)
5. Fraud Detection — Rule Engine vs ML
5.1 Rule-based Fraud Detection
type FraudRule interface {
Evaluate(ctx context.Context, tx Transaction, history TxHistory) FraudSignal
}
type VelocityRule struct {
MaxCountPerHour int
MaxAmountPerDay int64
}
func (r *VelocityRule) Evaluate(ctx context.Context, tx Transaction, history TxHistory) FraudSignal {
if history.CountLastHour() > r.MaxCountPerHour {
return FraudSignal{
Score: 0.8,
Reason: "high_velocity",
Action: ActionReview,
}
}
if history.SumLastDay() > r.MaxAmountPerDay {
return FraudSignal{Score: 0.9, Reason: "daily_limit_breach", Action: ActionBlock}
}
return FraudSignal{Score: 0.0, Action: ActionAllow}
}
// Rule engine chạy tất cả rules và aggregate
func (e *FraudEngine) Score(ctx context.Context, tx Transaction) Decision {
signals := make([]FraudSignal, 0, len(e.rules))
for _, rule := range e.rules {
history := e.historyStore.Get(ctx, tx.AccountID)
signals = append(signals, rule.Evaluate(ctx, tx, history))
}
return e.aggregator.Aggregate(signals) // max score, hoặc weighted sum
}
Ưu điểm của rule-based: Explainable (giải thích được tại sao block), dễ debug, dễ audit (compliance yêu cầu), low latency (< 5ms).
Nhược điểm: Fraudster adapt nhanh, rules cũ trở nên ineffective. Cần team chuyên maintain rules.
5.2 ML-based Fraud Scoring
Feature Engineering cho Fraud ML:
Transaction features:
- amount, merchant_category, time_of_day, day_of_week
- device_fingerprint, IP geolocation
- distance từ last transaction (velocity of location change)
Account features:
- account_age_days, kyc_level
- avg_transaction_amount (rolling 30d)
- merchant_diversity_score
Network features:
- recipient fraud score
- shared device với fraudulent accounts
Model: Gradient Boosting (XGBoost/LightGBM) cho batch scoring
Logistic Regression cho real-time (< 10ms SLA)
Graph Neural Network cho network fraud detection
Inference pipeline:
Transaction → Feature Store (Redis) → Model Server → Score
Latency budget: < 50ms total (fraud check là blocking)
5.3 Real-time vs Batch Fraud Detection
Real-time (synchronous, blocking):
Payment request → Fraud check → Allow/Block
SLA: < 100ms — nếu chậm hơn, timeout và allow (false negative)
Dùng: rules + lightweight ML
Batch (async, post-transaction):
Payment completed → Queue → Fraud analysis (minutes/hours later)
→ If fraud detected → freeze account, initiate chargeback
Dùng: heavy ML models, graph analysis
Layered approach:
Layer 1 (2ms): Device fingerprint check
Layer 2 (10ms): Rule engine
Layer 3 (30ms): Lightweight ML score
Layer 4 (async): Graph analysis (không blocking)
6. Card Processing — Authorization, Clearing, Chargeback
6.1 Card Transaction Flow
Cardholder POS Terminal Acquirer Card Network Issuer
│ │ │ (Visa/MC) │
│─── swipe/tap ─────►│ │ │ │
│ │─ auth request ──►│ │ │
│ │ │─ forward ─────►│ │
│ │ │ │─ auth req ───►│
│ │ │ │ │ (check balance,
│ │ │ │ │ fraud score)
│ │ │ │◄─ approved ───│
│ │ │◄─ approved ────│ │
│ │◄─ approved ──────│ │ │
│◄── receipt ────────│ │ │ │
│ │ │ │ │
│ [End of Day] │ │ │
│ │─ clearing file ─►│ │ │
│ │ │─ settlement ──►│ │
│ │ │ │─ settlement ─►│
Authorization (real-time, < 1s): Kiểm tra và "hold" tiền trên thẻ. Tiền chưa thực sự chuyển, chỉ reserve.
Clearing (end of day): Merchant gửi batch file xác nhận các giao dịch.
Settlement (T+1 hoặc T+2): Tiền thực sự di chuyển từ issuer sang acquirer.
6.2 Chargeback Flow
Chargeback xảy ra khi:
- Khách hàng dispute giao dịch ("tôi không mua cái này")
- Merchant fraud, hàng không giao
- Card bị compromised
Chargeback Flow:
Cardholder → files dispute với Issuer
Issuer → provisional credit cho cardholder
Issuer → chargeback notice tới Card Network
Card Network → forward tới Acquirer
Acquirer → debit từ Merchant account
Merchant → có 30 ngày để dispute (representment)
If Merchant wins → debit cardholder lại
If Merchant loses → chargeback permanent
Chargeback rate > 1% → Merchant bị Visa/MC phạt hoặc terminate
7. Vietnamese Fintech Context: VietQR, Napas
7.1 VietQR
VietQR là chuẩn QR code thống nhất cho thanh toán tại Việt Nam, ban hành bởi NHNN năm 2021. Thay thế cho việc mỗi ngân hàng có QR riêng (VCB Pay, Momo, ZaloPay đều khác nhau).
VietQR Code Structure (EMVCo format):
00: Payload format indicator = "01"
01: Point of initiation = "12" (dynamic)
38: Napas merchant info
00: GUID = "A000000727" (Napas identifier)
01: BankBin + AccountNo (số tài khoản)
52: Merchant category code
53: Transaction currency = "704" (VND)
54: Amount (optional, nếu dynamic)
58: Country code = "VN"
59: Merchant name
60: Merchant city
63: CRC checksum
type VietQRPayload struct {
BankBin string // VCB = "970436", Cake = "546034"
AccountNo string
Amount int64 // 0 nếu để user nhập
Message string // nội dung chuyển khoản
}
func GenerateVietQR(p VietQRPayload) string {
// Build EMVCo TLV format
merchantInfo := buildTLV("00", "A000000727") +
buildTLV("01", p.BankBin+p.AccountNo)
payload := buildTLV("00", "01") + // format indicator
buildTLV("01", "12") + // dynamic QR
buildTLV("38", merchantInfo) + // Napas merchant info
buildTLV("52", "5999") + // MCC: general retail
buildTLV("53", "704") + // VND
buildTLV("58", "VN") // Vietnam
if p.Amount > 0 {
payload += buildTLV("54", fmt.Sprintf("%d", p.Amount))
}
// Thêm CRC16 ở cuối
payload += buildTLV("63", computeCRC16(payload+"6304"))
return payload
}
7.2 Napas Interbank Transfer
Luồng chuyển tiền liên ngân hàng qua Napas:
Cake Backend Napas Switch VCB Backend
│ │ │
│── ISO 8583 message ──────────►│ │
│ (MTI 0200: Financial Req) │── forward ──────────────►│
│ │ │ verify account
│ │◄─ ISO 8583 0210 ─────────│
│◄── response (0210) ───────────│ (response code 00=OK) │
│ │ │
│ [Nếu approved] │ │
│── post ledger entries ────────│ │
│ debit Alice, credit Napas │ │
ISO 8583 là message format chuẩn cho card/interbank transactions:
MTI 0200 = Authorization Request
MTI 0210 = Authorization Response
MTI 0400 = Reversal Request
Field 39 = Response Code (00 = Approved, 51 = Insufficient funds, ...)
8. Interview: "Design a Payment System / Wallet System"
Interview insight: Câu hỏi này xuất hiện ở hầu hết mọi fintech interview. Examiner muốn thấy bạn biết về idempotency, double-entry ledger, và trade-off giữa consistency và availability.
Framework trả lời
1. Clarify requirements (2-3 phút):
- Internal wallet hay interbank?
- Latency SLA? (real-time hay T+1?)
- Scale? (TPS? MAU?)
- Currency? Multi-currency?
2. Core Data Model:
- accounts table
- ledger_entries table (immutable, double-entry)
- transactions table (saga state)
3. Transfer Flow:
- Validate → Reserve → Debit/Credit → Confirm
- Idempotency key ở mọi bước
4. Failure Scenarios:
- Timeout sau debit nhưng trước credit → reversal job
- DB down → retry với idempotency key
- Network partition → taint transaction, reconcile later
5. Scale:
- Sharding by account_id (consistent hashing)
- Hot account problem (celebrity account) → shard by user, aggregate writes
- Read replicas cho balance queries
Điểm thường bị trừ
❌ "Dùng UPDATE accounts SET balance = balance - amount WHERE id = ?"
→ Không có audit trail, không thể reconcile, không idempotent
✓ "Mỗi giao dịch tạo ra ledger entries, balance tính từ ledger"
❌ Quên idempotency → double charge khi retry
❌ Quên atomicity → debit xong nhưng credit fail
❌ Dùng floating point cho tiền
❌ Không mention reconciliation và settlement