🏭 Domains✍️ Khoa📅 19/04/2026☕ 14 phút đọc

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