Case Study: Design a Payment System
“Payment system giống như ngân hàng trung ương xử lý hàng triệu giao dịch mỗi ngày — mỗi đồng tiền phải chính xác tuyệt đối. Sai 1 cent trên 1 triệu giao dịch = sai 10,000 đồng. Nhân lên 365 ngày, kết quả là thảm họa tài chính.”
Tags: system-design payment-system fintech exactly-once ledger alex-xu-vol2 case-study Student: Hieu Prerequisite: Tuan-02-Back-of-the-envelope · Tuan-08-Message-Queue · Tuan-11-Microservices-Pattern Lien quan: Tuan-14-AuthN-AuthZ-Security · Tuan-15-Data-Security-Encryption · Tuan-11-Microservices-Pattern · Tuan-07-Database-Sharding-Replication Reference: Alex Xu, System Design Interview Volume 2 — Chapter 7: Payment System
Context & Why — Tại sao Payment System quan trọng?
Analogy: Ngân hàng trung ương
Hieu, hãy tưởng tượng em đang vận hành ngân hàng trung ương của một quốc gia. Mỗi ngày có hàng triệu giao dịch diễn ra — từ việc mua cà phê 30,000 VND đến chuyển khoản 500 triệu VND mua nhà. Mỗi giao dịch phải:
- Chính xác tuyệt đối — sai 1 đồng cũng không được
- Không bị mất — tiền đã trừ mà hàng không giao = thảm họa
- Không bị trùng — charge khách 2 lần = mất uy tín + kiện tụng
- Có thể kiểm tra lại — auditor hỏi giao dịch cách đây 3 năm, em phải trả lời được
- Tuân thủ pháp luật — vi phạm PCI-DSS hoặc AML = phạt hàng triệu đô + mất license
Tại sao Backend Dev cần hiểu Payment System?
| Lý do | Giải thích |
|---|---|
| Mọi sản phẩm đều cần payment | E-commerce, SaaS, marketplace, subscription — tất cả cần thu tiền |
| Sai sót = mất tiền thật | Khác với chat system (mất tin nhắn = khó chịu), payment (mất tiền = kiện tụng) |
| Complexity ẩn giấu | Trông đơn giản (gửi tiền từ A → B), nhưng thực tế có hàng chục edge case |
| Interview favorite | Payment system là bài yêu thích của Big Tech vì đòi hỏi hiểu sâu về distributed systems |
| Regulation-heavy | Phải hiểu PCI-DSS, AML, KYC — không phải chỉ engineering |
Payment System trong thực tế
Các hệ thống payment nổi tiếng: Stripe, PayPal, Adyen, Square, Shopify Payments, VNPay, MoMo, ZaloPay.
Mỗi ngày:
- Stripe xử lý hàng trăm tỷ USD giao dịch
- PayPal xử lý ~40 triệu transactions/day
- Visa network xử lý ~150 triệu transactions/day
Key insight: Payment system không chỉ là “chuyển tiền”. Nó là sự kết hợp của distributed systems, database consistency, security, compliance, và business logic — tất cả phải hoạt động chính xác 100%.
Step 1 — Understand the Problem & Establish Design Scope
1.1 Clarifying Questions (Câu hỏi làm rõ)
Trong interview, luôn hỏi trước khi thiết kế. Dưới đây là các câu hỏi quan trọng và câu trả lời giả định:
| Câu hỏi | Trả lời | Ghi chú |
|---|---|---|
| Pay-in (nhận tiền) hay pay-out (gửi tiền)? | Cả hai | Pay-in: buyer trả merchant. Pay-out: platform trả merchant |
| Payment methods nào? | Credit/debit card, digital wallet | Qua PSP (Stripe, PayPal) |
| Scale bao lớn? | 1M transactions/day | Quy mô Shopify-like |
| Cần support multi-currency? | Có | USD, EUR, VND, v.v. |
| Exactly-once semantics? | Bắt buộc | Không được charge 2 lần |
| Reconciliation? | Có | Đối soát hàng ngày |
| Refund support? | Có | Partial và full refund |
| Wallet service? | Có | Lưu số dư merchant |
| Compliance requirements? | PCI-DSS Level 1 | Vì xử lý card data |
1.2 Functional Requirements (Yêu cầu chức năng)
- FR1: Pay-in flow — Buyer thanh toán cho merchant qua nhiều payment methods (credit card, debit card, digital wallet)
- FR2: Pay-out flow — Platform chuyển tiền cho merchant (settlement)
- FR3: Ledger — Ghi nhận mọi giao dịch theo chuẩn double-entry bookkeeping
- FR4: Wallet — Quản lý số dư tài khoản của merchant trên platform
- FR5: Reconciliation — Đối soát giữa internal ledger, PSP settlement, và bank statement
- FR6: Refund — Hoàn tiền cho buyer (partial hoặc full)
- FR7: Multi-currency — Xử lý giao dịch đa tiền tệ với exchange rate
- FR8: Payment status tracking — Buyer và merchant xem được trạng thái giao dịch real-time
1.3 Non-functional Requirements (Yêu cầu phi chức năng)
| Yêu cầu | Mục tiêu | Giải thích |
|---|---|---|
| Reliability (Độ tin cậy) | 99.99% — không mất giao dịch | Mất 1 giao dịch = mất tiền thật |
| Exactly-once (Chính xác một lần) | Không duplicate payment | Charge khách 2 lần = thảm họa |
| Consistency (Nhất quán) | Strong consistency cho balance | Số dư phải luôn chính xác |
| Availability (Sẵn sàng) | 99.99% uptime (~52 phút downtime/năm) | Payment down = revenue down |
| Auditability (Kiểm tra được) | Mọi giao dịch có audit trail | Compliance requirement |
| Security | PCI-DSS Level 1 | Bảo vệ card data |
| Latency | P99 < 1s cho payment request | User experience |
Trade-off quan trọng: Trong payment system, Consistency > Availability. Đây là hệ thống CP (theo CAP theorem). Tham chiếu Tuan-07-Database-Sharding-Replication để hiểu CP vs AP.
1.4 Phân biệt Pay-in vs Pay-out
| Đặc điểm | Pay-in (Thu tiền) | Pay-out (Chi tiền) |
|---|---|---|
| Hướng tiền | Buyer → Merchant (qua platform) | Platform → Merchant (settlement) |
| Trigger | Buyer click “Pay” | Scheduled (daily/weekly) hoặc manual |
| Speed | Real-time (vài giây) | Batch (1-3 ngày làm việc) |
| Volume | Cao (mỗi đơn hàng) | Thấp hơn (aggregate nhiều đơn) |
| Risk | Fraud, chargeback | Insufficient fund, wrong account |
| Ví dụ | Bạn mua áo trên Shopee, trả 500K | Shopee chuyển 450K cho seller cuối tuần |
Step 2 — High-Level Design
2.1 System Components Overview
Payment system bao gồm các component chính:
| Component | Vai trò | Analogy |
|---|---|---|
| Payment Service | Orchestrator — điều phối toàn bộ payment flow | Giám đốc ngân hàng |
| PSP (Payment Service Provider) | Xử lý thanh toán với card network/bank | Visa/Mastercard processing center |
| Ledger Service | Ghi nhận mọi giao dịch (double-entry) | Sổ cái kế toán |
| Wallet Service | Quản lý số dư tài khoản | Ví tiền merchant |
| Reconciliation Service | Đối soát dữ liệu giữa các hệ thống | Kiểm toán viên |
| Fraud Detection Service | Phát hiện giao dịch gian lận | Cảnh sát tài chính |
2.2 High-Level Architecture
flowchart TB subgraph "Client Layer" BUYER[Buyer<br/>Web / Mobile App] MERCHANT[Merchant<br/>Dashboard] end subgraph "API Gateway" GW[API Gateway<br/>Rate Limiting, Auth, Routing] end subgraph "Core Payment Services" PS[Payment Service<br/>Orchestrator] LEDGER[Ledger Service<br/>Double-entry Bookkeeping] WALLET[Wallet Service<br/>Balance Management] end subgraph "External" PSP[PSP<br/>Stripe / PayPal / Adyen] CARD[Card Network<br/>Visa / Mastercard] BANK[Issuing Bank<br/>Buyer's Bank] MBANK[Acquiring Bank<br/>Merchant's Bank] end subgraph "Supporting Services" RECON[Reconciliation Service<br/>Daily/Hourly Batch] FRAUD[Fraud Detection<br/>Rule Engine + ML] NOTIFY[Notification Service<br/>Email / SMS / Push] CURRENCY[Currency Service<br/>Exchange Rate] end subgraph "Data Stores" PGDB[(PostgreSQL<br/>Payment Records)] LEDGERDB[(Ledger DB<br/>Immutable Append-only)] WALLETDB[(Wallet DB<br/>Balance Store)] MQ[Message Queue<br/>Kafka] end BUYER --> GW MERCHANT --> GW GW --> PS PS --> PSP PSP --> CARD CARD --> BANK CARD --> MBANK PS --> LEDGER PS --> WALLET PS --> FRAUD PS --> CURRENCY LEDGER --> LEDGERDB WALLET --> WALLETDB PS --> PGDB PS --> MQ MQ --> RECON MQ --> NOTIFY RECON --> LEDGERDB
2.3 Pay-in Flow Overview
Khi buyer click “Pay” trên website:
1. Buyer → API Gateway → Payment Service: "Tôi muốn trả 500,000 VND cho đơn hàng #12345"
2. Payment Service → Fraud Detection: "Giao dịch này có đáng ngờ không?"
3. Fraud Detection → Payment Service: "OK, hợp lệ"
4. Payment Service → PSP (Stripe): "Charge card **** 4242, số tiền 500,000 VND"
5. PSP → Card Network (Visa) → Issuing Bank: "Authorize 500,000 VND"
6. Bank → Card Network → PSP → Payment Service: "Approved"
7. Payment Service → Ledger: "Ghi nhận: Debit buyer 500K, Credit merchant 500K"
8. Payment Service → Wallet: "Cộng 500K vào wallet merchant"
9. Payment Service → Notification: "Gửi email xác nhận cho buyer & merchant"
10. Payment Service → Buyer: "Thanh toán thành công!"
2.4 Pay-out Flow Overview
Khi platform chuyển tiền cho merchant:
1. Scheduler trigger (mỗi ngày 00:00 UTC)
2. Pay-out Service: Tổng hợp tất cả giao dịch chưa settle cho merchant X
3. Pay-out Service: Trừ platform fee (ví dụ: 2.9% + $0.30 per transaction)
4. Pay-out Service → PSP: "Transfer 45,000,000 VND cho merchant X, bank account YYY"
5. PSP → Acquiring Bank → Merchant's Bank Account
6. Ledger: Ghi nhận settlement entry
7. Wallet: Trừ số dư merchant wallet
8. Notification: "Settlement 45,000,000 VND đã được chuyển"
Step 3 — Deep Dive
3.1 Payment Flow End-to-End (Chi tiết)
Flow diagram
sequenceDiagram participant B as Buyer participant FE as Frontend participant PS as Payment Service participant FD as Fraud Detection participant PSP as PSP (Stripe) participant CN as Card Network (Visa) participant IB as Issuing Bank participant L as Ledger participant W as Wallet participant N as Notification B->>FE: Click "Pay 500K VND" FE->>PS: POST /payments {order_id, amount, currency, method} Note over PS: Generate idempotency_key<br/>Save payment record (PENDING) PS->>FD: Check fraud risk FD-->>PS: Risk score: LOW (pass) PS->>PSP: Create charge (with idempotency_key) Note over PSP: Redirect to hosted payment page<br/>Buyer enters card details PSP->>CN: Authorization request CN->>IB: Authorize 500K VND IB-->>CN: Approved (auth_code: ABC123) CN-->>PSP: Approved PSP-->>PS: Webhook: payment.success {charge_id, auth_code} Note over PS: Update payment status → SUCCESS PS->>L: Record double-entry Note over L: Debit: buyer_payable 500K<br/>Credit: merchant_receivable 500K PS->>W: Credit merchant wallet +500K PS->>N: Send confirmation N-->>B: Email: "Payment successful"
Các giai đoạn của Payment Flow
| Giai đoạn | Mô tả | Thời gian | Failure mode |
|---|---|---|---|
| 1. Initiation | Buyer submit payment request | Instant | Validation error |
| 2. Fraud Check | Kiểm tra gian lận | 50-200ms | False positive (block legit payment) |
| 3. Authorization | PSP gửi auth request qua card network tới issuing bank | 1-3s | Card declined, insufficient funds |
| 4. Capture | Thực hiện charge (có thể tách khỏi auth) | 0-24h | Capture expired |
| 5. Recording | Ghi ledger + update wallet | 50-100ms | DB write failure |
| 6. Notification | Thông báo cho buyer/merchant | Async | Notification lost (non-critical) |
| 7. Settlement | PSP chuyển tiền thực tế cho merchant | 1-3 business days | Bank transfer failure |
Authorization vs Capture: Trong nhiều use case (hotel booking, car rental), authorize trước nhưng capture sau. Authorization “giữ” tiền trong tài khoản buyer nhưng chưa thực sự charge. Capture mới thực sự rút tiền.
3.2 PSP Integration — Tích hợp Payment Service Provider
Tại sao dùng PSP thay vì tự build?
| Tự build | Dùng PSP (Stripe/PayPal) |
|---|---|
| Phải tuân thủ PCI-DSS Level 1 (tốn 2M/năm để audit) | PSP chịu phần lớn PCI-DSS compliance |
| Phải tích hợp trực tiếp với từng card network (Visa, Mastercard, AMEX, JCB) | PSP đã tích hợp sẵn 100+ payment methods |
| Phải xử lý fraud detection, chargeback, dispute | PSP cung cấp fraud tools built-in |
| Cần team 50+ engineers chuyên payment | Tích hợp API trong vài tuần |
| Risk cực cao — sai sót = mất license | PSP chịu liability |
Verdict: Trừ khi em là Stripe hay PayPal, luôn dùng PSP. Ngay cả Shopify — một trong những e-commerce platform lớn nhất — cũng dùng Stripe làm PSP.
Hosted Payment Page
PSP cung cấp hosted payment page — trang thanh toán do PSP host:
1. Buyer click "Pay" trên website merchant
2. Frontend redirect đến Stripe Checkout page (hoặc embed Stripe Elements)
3. Buyer nhập card number, expiry, CVV trên trang của Stripe
4. Stripe xử lý payment, trả kết quả về cho merchant qua webhook
Lợi ích của hosted payment page:
| Lợi ích | Giải thích |
|---|---|
| PCI-DSS scope reduction | Card data KHÔNG BAO GIỜ đi qua server của merchant |
| Trust | Buyer thấy trang Stripe/PayPal → tin tưởng hơn |
| Auto-update | Stripe tự cập nhật 3D Secure, new card networks |
| Liability shift | Nếu fraud xảy ra, PSP chịu liability (trong nhiều trường hợp) |
Tokenization
Khi buyer nhập card number lần đầu, PSP tokenize card:
Card: 4242 4242 4242 4242
↓ (PSP tokenization)
Token: tok_1MqZ8bCjKG4sJv2D
| Khái niệm | Giải thích |
|---|---|
| Token | Chuỗi random đại diện cho card, không thể reverse-engineer về card number |
| Vault | PSP lưu card data trong encrypted vault (PCI-DSS certified) |
| Reuse | Merchant lưu token để charge lần sau (subscription, 1-click buy) |
| Scope | Token chỉ valid cho merchant đã tạo nó |
Rule vàng: KHÔNG BAO GIỜ lưu card number trên server của em. Chỉ lưu token từ PSP. Vi phạm rule này = vi phạm PCI-DSS = phạt hàng triệu đô + mất quyền xử lý card.
Webhook Pattern
PSP thông báo kết quả payment qua webhook (HTTP POST callback):
PSP → Merchant server: POST /webhooks/stripe
{
"type": "payment_intent.succeeded",
"data": {
"id": "pi_3MqBv2CjKG4sJv2D",
"amount": 500000,
"currency": "vnd",
"status": "succeeded"
}
}
Xử lý webhook đúng cách:
| Best Practice | Lý do |
|---|---|
| Verify signature | Đảm bảo webhook thực sự từ PSP, không phải attacker |
| Idempotent handling | PSP có thể gửi cùng webhook nhiều lần (retry) |
| Return 200 nhanh | Xử lý async, return 200 ngay để PSP không retry |
| Persist before process | Lưu webhook event vào DB trước khi xử lý business logic |
| Dead letter queue | Webhook processing fail → đẩy vào DLQ để retry sau |
3.3 Double-Entry Ledger — Sổ cái kép
Tại sao cần Double-Entry?
Double-entry bookkeeping là nguyên tắc kế toán có từ thế kỷ 15 (Luca Pacioli, 1494). Mỗi giao dịch phải có ít nhất 2 entries: một debit và một credit, với tổng bằng nhau.
Accounting equation (phương trình kế toán cơ bản):
Aha Moment: Phương trình này LUÔN phải cân bằng. Nếu bất kỳ lúc nào Assets khác Liabilities + Equity, nghĩa là có bug hoặc fraud. Đây chính là cơ chế “self-checking” của hệ thống kế toán.
Double-Entry trong Payment System
Khi buyer trả 500,000 VND cho merchant:
| Entry | Account | Type | Amount |
|---|---|---|---|
| 1 | Buyer Cash (Asset) | Debit (tăng) | 500,000 VND |
| 2 | Platform Revenue (Liability) | Credit (tăng) | 500,000 VND |
Khi platform settle cho merchant (trừ 2% fee = 10,000 VND):
| Entry | Account | Type | Amount |
|---|---|---|---|
| 3 | Platform Revenue (Liability) | Debit (giảm) | 500,000 VND |
| 4 | Merchant Payout (Asset) | Credit (tăng) | 490,000 VND |
| 5 | Platform Fee Income (Equity/Revenue) | Credit (tăng) | 10,000 VND |
Kiểm tra: Tổng Debit = 500,000 + 500,000 = 1,000,000. Tổng Credit = 500,000 + 490,000 + 10,000 = 1,000,000. Cân bằng!
Ledger Data Model
| Field | Type | Description |
|---|---|---|
ledger_entry_id | UUID | Primary key |
transaction_id | UUID | Nhóm các entries cùng 1 giao dịch |
account_id | UUID | Tài khoản bị ảnh hưởng |
entry_type | ENUM | DEBIT hoặc CREDIT |
amount | BIGINT | Số tiền (đơn vị nhỏ nhất — cents/đồng) |
currency | VARCHAR(3) | ISO 4217 (VND, USD, EUR) |
created_at | TIMESTAMP | Thời điểm tạo |
description | TEXT | Mô tả giao dịch |
idempotency_key | VARCHAR | Chống duplicate entry |
Đặc điểm quan trọng của Ledger:
| Đặc điểm | Giải thích |
|---|---|
| Append-only | KHÔNG BAO GIỜ update hay delete entry. Sai → tạo reversal entry |
| Immutable | Mỗi entry là bất biến sau khi ghi |
| Complete | Mọi giao dịch đều phải có ledger entry |
| Balanced | Tổng debit = tổng credit cho mỗi transaction |
| Auditable | Có thể trace lại mọi thay đổi balance |
Tại sao append-only? Nếu cho phép update/delete, không thể audit. Khi phát hiện sai sót, tạo reversal entry (giao dịch đảo ngược) thay vì sửa entry cũ. Đây là nguyên tắc bất biến trong kế toán.
Double-Entry Ledger Flow
flowchart LR subgraph "Payment Event" PAY[Buyer pays 500K VND] end subgraph "Ledger Entries" D1[DEBIT<br/>Buyer Cash Account<br/>+500,000 VND] C1[CREDIT<br/>Platform Holding<br/>+500,000 VND] end subgraph "Validation" CHECK{Sum Debit<br/>= Sum Credit?} end subgraph "Result" OK[Entry Committed] FAIL[Entry Rejected<br/>Alert Triggered] end PAY --> D1 PAY --> C1 D1 --> CHECK C1 --> CHECK CHECK -->|Yes| OK CHECK -->|No| FAIL style D1 fill:#e53935,color:#fff style C1 fill:#43a047,color:#fff style OK fill:#1e88e5,color:#fff style FAIL fill:#ff6f00,color:#fff
3.4 Exactly-Once Delivery — Ngữ nghĩa chính xác một lần
Tại sao Exactly-Once là vấn đề khó nhất?
Trong distributed systems, có 3 mức delivery guarantee:
| Guarantee | Ý nghĩa | Rủi ro |
|---|---|---|
| At-most-once | Gửi 1 lần, không retry | Mất payment → buyer trả tiền nhưng không nhận hàng |
| At-least-once | Retry nếu không nhận response | Duplicate payment → buyer bị charge 2 lần |
| Exactly-once | Đảm bảo xử lý đúng 1 lần | Không có rủi ro — nhưng KHÓ NHẤT để implement |
Sự thật phũ phàng: Trong distributed systems, exactly-once delivery là bất khả thi (theo lý thuyết). Nhưng chúng ta có thể đạt exactly-once processing bằng cách kết hợp at-least-once delivery + idempotency.
Idempotency Key Pattern
Idempotency (tính lũy đẳng): thực hiện cùng 1 operation nhiều lần cho cùng kết quả như thực hiện 1 lần.
Request 1: POST /payments {idempotency_key: "abc123", amount: 500000}
→ Payment created, return payment_id: "pay_001"
Request 2: POST /payments {idempotency_key: "abc123", amount: 500000}
→ Return existing payment_id: "pay_001" (KHÔNG tạo payment mới)
Request 3: POST /payments {idempotency_key: "abc123", amount: 500000}
→ Return existing payment_id: "pay_001" (vẫn idempotent)
Implementation:
| Bước | Hành động |
|---|---|
| 1 | Client generate idempotency_key (UUID v4) |
| 2 | Server nhận request, check DB: SELECT * FROM payments WHERE idempotency_key = ? |
| 3a | Nếu không tồn tại → tạo payment mới, lưu idempotency_key |
| 3b | Nếu đã tồn tại → trả về kết quả cũ (cached response) |
| 4 | Response trả về cho client |
Xử lý race condition: Hai request cùng idempotency_key đến cùng lúc?
| Approach | Cách hoạt động | Trade-off |
|---|---|---|
| DB unique constraint | UNIQUE INDEX ON idempotency_key → request thứ 2 bị constraint violation | Đơn giản, nhưng cần handle error |
| Distributed lock | Acquire lock trên idempotency_key trước khi xử lý | An toàn hơn, nhưng tốn performance |
| Optimistic locking | Check + insert with version, retry nếu conflict | Tốt cho low-contention |
Best practice từ Stripe: Stripe require client gửi
Idempotency-Keyheader cho mọi POST request. Key valid trong 24 giờ. Nếu retry với cùng key nhưng body khác → return 422 error.
Retry with Idempotency
Khi network timeout xảy ra, client không biết payment thành công hay thất bại:
Client → Server: POST /payments {idempotency_key: "abc123"}
Server: Xử lý xong, gửi response
Network: *timeout* — response bị mất
Client: Không nhận được response → RETRY
Client → Server: POST /payments {idempotency_key: "abc123"}
Server: Check DB → đã xử lý → trả về kết quả cũ
Client: Nhận response → OK!
Retry strategy:
| Strategy | Mô tả | Khi nào dùng |
|---|---|---|
| Exponential backoff | Wait 1s, 2s, 4s, 8s, 16s… | Default cho mọi retry |
| Jitter | Thêm random delay để tránh thundering herd | Luôn kết hợp với exponential backoff |
| Max retries | Giới hạn số lần retry (ví dụ: 5 lần) | Tránh retry vô hạn |
| Circuit breaker | Ngừng retry nếu service liên tục fail | Bảo vệ downstream service |
Aha Moment: Idempotency key là THE most important concept trong payment system. Nếu em chỉ nhớ được 1 thứ từ bài này, hãy nhớ idempotency. Mọi thứ khác đều xây dựng trên nền tảng này.
Payment State Machine
Payment KHÔNG phải là một function call đơn giản (gọi → xong). Payment là state machine — một chuỗi trạng thái có chuyển đổi rõ ràng:
stateDiagram-v2 [*] --> CREATED: Buyer submits payment CREATED --> FRAUD_CHECK: Validate & check fraud FRAUD_CHECK --> REJECTED: Fraud detected FRAUD_CHECK --> PENDING: Fraud check passed PENDING --> PROCESSING: Send to PSP PROCESSING --> AUTHORIZED: Bank approved PROCESSING --> FAILED: Bank declined AUTHORIZED --> CAPTURED: Capture payment AUTHORIZED --> VOIDED: Cancel before capture AUTHORIZED --> EXPIRED: Auth expired (7-30 days) CAPTURED --> SETTLED: PSP settled funds CAPTURED --> REFUND_PENDING: Refund requested REFUND_PENDING --> REFUNDED: Refund completed REFUND_PENDING --> REFUND_FAILED: Refund failed REFUND_FAILED --> REFUND_PENDING: Retry refund FAILED --> [*] REJECTED --> [*] SETTLED --> [*] REFUNDED --> [*] VOIDED --> [*] EXPIRED --> [*]
Các trạng thái quan trọng:
| State | Ý nghĩa | Action |
|---|---|---|
| CREATED | Payment request vừa được tạo | Validate input, generate idempotency key |
| FRAUD_CHECK | Đang kiểm tra gian lận | Rule engine + ML model |
| PENDING | Chờ gửi đến PSP | Enqueue for processing |
| PROCESSING | Đang xử lý tại PSP/Bank | Waiting for async response |
| AUTHORIZED | Bank đã approve, tiền bị “hold” | Chưa charge thực sự |
| CAPTURED | Tiền đã bị charge thực sự | Ledger entry created |
| SETTLED | PSP đã chuyển tiền cho merchant | Settlement complete |
| FAILED | Payment thất bại | Notify buyer, log reason |
| REFUNDED | Đã hoàn tiền | Reversal ledger entry created |
| VOIDED | Hủy trước khi capture | Release hold on buyer’s card |
Tại sao state machine quan trọng? Vì payment có thể ở trạng thái “in-limbo” (PROCESSING) trong vài giây đến vài phút. Nếu không có state machine, em không biết payment đang ở đâu → không biết nên retry hay chờ → dẫn đến duplicate charge hoặc lost payment.
Allowed state transitions: Chỉ các transition được define trong state machine mới hợp lệ. Ví dụ:
- CREATED → CAPTURED: INVALID (không thể skip fraud check & authorization)
- SETTLED → PROCESSING: INVALID (không thể quay lại trạng thái trước)
- CAPTURED → REFUND_PENDING: VALID
3.5 Reconciliation — Đối soát
Tại sao cần Reconciliation?
Trong hệ thống payment, có 3 nguồn sự thật (sources of truth):
| Source | Dữ liệu | Ai quản lý |
|---|---|---|
| Internal Ledger | Mọi giao dịch platform ghi nhận | Platform (chúng ta) |
| PSP Settlement Report | Giao dịch PSP đã xử lý | Stripe/PayPal |
| Bank Statement | Tiền thực tế vào/ra tài khoản ngân hàng | Bank |
Ba nguồn này phải khớp nhau. Nếu không khớp → có vấn đề (bug, fraud, hoặc timing difference).
Analogy: Giống như em kiểm tra sổ chi tiêu cá nhân (internal ledger) với lịch sử giao dịch trên app ngân hàng (bank statement) mỗi cuối tháng. Nếu có giao dịch em không nhận ra → có thể bị hack thẻ.
Reconciliation Pipeline
flowchart TB subgraph "Data Sources" IL[Internal Ledger<br/>Real-time] PSP_R[PSP Settlement Report<br/>Daily file T+1] BANK_S[Bank Statement<br/>Daily/Hourly via API] end subgraph "Reconciliation Engine" FETCH[Fetch & Normalize<br/>Convert to common format] MATCH[Matching Engine<br/>Match by transaction_id, amount, date] DIFF[Discrepancy Detection<br/>Find unmatched records] end subgraph "Results" MATCHED[Matched Records<br/>Everything OK] MISMATCH[Mismatched Records<br/>Amount differs] MISSING_INT[Missing Internal<br/>PSP has, we don't] MISSING_EXT[Missing External<br/>We have, PSP doesn't] end subgraph "Actions" AUTO[Auto-resolve<br/>Timing difference] ALERT[Alert Team<br/>Needs investigation] TICKET[Create Ticket<br/>Manual resolution] end IL --> FETCH PSP_R --> FETCH BANK_S --> FETCH FETCH --> MATCH MATCH --> DIFF DIFF --> MATCHED DIFF --> MISMATCH DIFF --> MISSING_INT DIFF --> MISSING_EXT MATCHED --> AUTO MISMATCH --> ALERT MISSING_INT --> ALERT MISSING_EXT --> TICKET style MISMATCH fill:#e53935,color:#fff style MISSING_INT fill:#ff6f00,color:#fff style MISSING_EXT fill:#ff6f00,color:#fff style MATCHED fill:#43a047,color:#fff
Các loại Discrepancy (sai lệch)
| Loại | Mô tả | Nguyên nhân phổ biến | Cách xử lý |
|---|---|---|---|
| Timing difference | Giao dịch ngày 31/12 trong ledger nhưng PSP report ngày 01/01 | Timezone, batch processing cutoff | Auto-resolve: match lại ngày hôm sau |
| Amount mismatch | Internal: 500K, PSP: 499.7K | PSP fee bị trừ trước, currency rounding | Kiểm tra fee structure, FX rate |
| Missing in internal | PSP report có giao dịch mà ledger không có | Webhook failed, DB write failed | Investigate → manual adjustment |
| Missing in PSP | Ledger có nhưng PSP không report | Payment still processing, PSP bug | Wait T+2, contact PSP support |
| Duplicate | Cùng giao dịch xuất hiện 2 lần | Idempotency failure | Tạo reversal entry cho bản trùng |
| Status mismatch | Internal: SUCCESS, PSP: FAILED | Webhook order, race condition | Rollback: refund buyer, alert merchant |
Reconciliation Schedule
| Tần suất | Đối soát gì | Mục đích |
|---|---|---|
| Hourly | Internal ledger vs payment DB | Phát hiện internal inconsistency nhanh |
| Daily (T+1) | Internal ledger vs PSP settlement file | Main reconciliation |
| Weekly | Aggregated balance vs bank statement | Cross-check tổng tiền |
| Monthly | Full audit reconciliation | Compliance reporting |
Aha Moment: Reconciliation là “safety net” cuối cùng. Dù idempotency, state machine, retry logic có tốt đến đâu, vẫn cần reconciliation để bắt mọi sai sót mà code không catch được. Reconciliation catches everything.
3.6 Wallet Service — Quản lý số dư
Wallet Service làm gì?
Wallet service quản lý balance (số dư) của merchant trên platform. Mỗi merchant có một wallet, và wallet phải luôn chính xác.
| Operation | Mô tả | Ví dụ |
|---|---|---|
| Credit (cộng tiền) | Buyer thanh toán thành công | +500,000 VND |
| Debit (trừ tiền) | Settlement cho merchant | -490,000 VND |
| Hold (giữ tiền) | Chờ xử lý dispute/chargeback | Hold 500,000 VND |
| Release | Giải phóng hold | Release 500,000 VND |
Concurrency Problem — Vấn đề đồng thời
Khi 100 buyer trả tiền cho cùng 1 merchant cùng lúc → 100 concurrent writes vào cùng 1 wallet balance:
Thread 1: Read balance = 1,000,000 → Add 500,000 → Write 1,500,000
Thread 2: Read balance = 1,000,000 → Add 300,000 → Write 1,300,000
↑ Lost update!
(Kết quả đúng: 1,800,000 nhưng thực tế: 1,300,000 hoặc 1,500,000)
Optimistic vs Pessimistic Locking
| Approach | Cơ chế | Ưu điểm | Nhược điểm | Khi nào dùng |
|---|---|---|---|---|
| Pessimistic Locking | SELECT ... FOR UPDATE — lock row trước khi read | Đảm bảo consistency 100% | Performance thấp khi high contention | Balance update (ít conflict) |
| Optimistic Locking | Read version → Update WHERE version = old_version → Retry nếu conflict | Performance cao | Nhiều retry khi high contention | Read-heavy, low-conflict scenarios |
| Database-level atomic | UPDATE wallet SET balance = balance + 500000 WHERE id = ? | Đơn giản, atomic | Không check negative balance | Simple credit operations |
Pessimistic Locking flow:
BEGIN TRANSACTION;
SELECT balance, version FROM wallets WHERE merchant_id = 'M001' FOR UPDATE;
-- balance = 1,000,000, version = 5
-- Row is now LOCKED — other transactions must wait
-- Business logic: check balance, calculate new balance
-- new_balance = 1,000,000 + 500,000 = 1,500,000
UPDATE wallets SET balance = 1,500,000, version = 6 WHERE merchant_id = 'M001';
COMMIT;
-- Lock released
Optimistic Locking flow:
-- Step 1: Read (no lock)
SELECT balance, version FROM wallets WHERE merchant_id = 'M001';
-- balance = 1,000,000, version = 5
-- Step 2: Update with version check
UPDATE wallets SET balance = 1,500,000, version = 6
WHERE merchant_id = 'M001' AND version = 5;
-- If affected_rows = 0 → someone else updated → RETRY from Step 1
-- If affected_rows = 1 → SUCCESS
Recommendation: Cho payment system, dùng pessimistic locking cho balance updates. Lý do: tiền phải chính xác 100%, performance tradeoff chấp nhận được vì mỗi merchant thường không có quá nhiều concurrent transactions.
Distributed Transaction — Saga Pattern
Khi payment cần update nhiều services cùng lúc (payment DB + ledger + wallet), có 2 approaches:
| Approach | Mô tả | Trade-off |
|---|---|---|
| 2PC (Two-Phase Commit) | Coordinator lock tất cả resources → commit cùng lúc | Strong consistency nhưng blocking, single point of failure |
| Saga Pattern | Chuỗi local transactions, mỗi step có compensating action | Eventually consistent, non-blocking, nhưng complex |
Saga Pattern cho Payment — tham chiếu Tuan-11-Microservices-Pattern:
| Step | Service | Action | Compensating Action (nếu fail) |
|---|---|---|---|
| 1 | Payment Service | Create payment record (PROCESSING) | Mark payment as FAILED |
| 2 | Fraud Service | Check fraud → pass | N/A (read-only) |
| 3 | PSP | Charge card | Refund via PSP |
| 4 | Ledger | Create double-entry | Create reversal entry |
| 5 | Wallet | Credit merchant balance | Debit merchant balance |
| 6 | Notification | Send confirmation | Send failure notification |
Nếu Step 4 (Ledger) fail:
- Compensate Step 3: Gọi PSP refund
- Compensate Step 1: Mark payment as FAILED
- Steps 5 & 6: Không cần compensate (chưa thực hiện)
3.7 Handling Failures — Xử lý lỗi
Timeout Handling — Payment “in limbo”
Đây là scenario đáng sợ nhất trong payment system:
Payment Service → PSP: "Charge 500K VND"
... 30 giây trôi qua ... không có response ...
Payment đang ở trạng thái “in limbo” — không biết thành công hay thất bại. Nếu:
- Retry → có thể charge 2 lần
- Không retry → buyer có thể đã bị charge mà không nhận hàng
Giải pháp:
| Strategy | Cơ chế | Khi nào |
|---|---|---|
| Idempotent retry | Retry với cùng idempotency_key → PSP trả cached result | Sau timeout |
| Payment status check | Gọi GET /payments/{id} trên PSP để kiểm tra trạng thái | Trước khi retry |
| Timeout + async reconciliation | Mark payment as UNKNOWN → reconciliation service resolve sau | Khi PSP không response |
| Payment in limbo queue | Đẩy vào queue riêng → background worker check status mỗi 5 phút | Automated recovery |
Rule: Khi timeout, KHÔNG bao giờ assume payment failed. Luôn check status trước. Tiền có thể đã bị charge.
Compensation Transactions — Giao dịch bù
Khi payment đã thành công nhưng cần hoàn lại (refund, chargeback, error):
| Scenario | Compensation |
|---|---|
| Buyer request refund | Tạo refund transaction qua PSP |
| Chargeback (buyer dispute với bank) | Bank tự lấy tiền lại, platform chịu phí |
| Double charge phát hiện qua reconciliation | Tạo reversal entry + refund |
| Merchant fraud | Freeze wallet + hold settlement |
Compensation flow:
Original: Debit buyer 500K, Credit merchant 500K
Refund: Debit merchant 500K, Credit buyer 500K (reversal)
Quan trọng: KHÔNG BAO GIỜ delete original entry. Tạo reversal entry riêng. Audit trail phải complete.
Dead Letter Queue (DLQ)
Khi message processing fail sau nhiều lần retry:
Main Queue → Consumer: Process payment webhook
Consumer: FAIL (3 retries exhausted)
Consumer → DLQ: Move failed message to Dead Letter Queue
Alert → On-call engineer: "5 messages in DLQ — investigate!"
| DLQ Strategy | Mô tả |
|---|---|
| Auto-retry with delay | DLQ consumer retry mỗi 1 giờ |
| Manual investigation | Engineer xem DLQ, fix root cause, replay message |
| Alert threshold | > 10 messages in DLQ within 1 hour → PagerDuty alert |
| Max retention | DLQ messages giữ 14 ngày, sau đó archive to cold storage |
3.8 Currency Handling — Xử lý tiền tệ
Store Amounts in Smallest Unit
Rule: Luôn lưu tiền ở đơn vị nhỏ nhất (cents cho USD, đồng cho VND):
| Currency | Đơn vị nhỏ nhất | Ví dụ | Stored value |
|---|---|---|---|
| USD | cent | $49.99 | 4999 |
| EUR | cent | 29.50 EUR | 2950 |
| VND | đồng | 500,000 VND | 500000 |
| JPY | yen (không có subunit) | 5,000 JPY | 5000 |
| BHD | fils (1/1000) | 10.500 BHD | 10500 |
Tại sao? Floating-point arithmetic KHÔNG chính xác.
0.1 + 0.2 = 0.30000000000000004trong hầu hết ngôn ngữ lập trình. Dùng integer (BIGINT) → chính xác 100%.
Exchange Rate Service
Khi buyer trả USD nhưng merchant nhận VND:
| Step | Action |
|---|---|
| 1 | Buyer pays $20 USD |
| 2 | Exchange rate service: 1 USD = 25,450 VND (rate tại thời điểm giao dịch) |
| 3 | Merchant receives 509,000 VND |
| 4 | Lưu cả original amount ($20), converted amount (509,000 VND), và exchange rate (25,450) |
Best practices:
- Lock exchange rate tại thời điểm payment initiation (không thay đổi giữa chừng)
- Store rate snapshot — lưu rate tại thời điểm giao dịch, không rely on current rate
- Rounding rules — theo ISO 4217, mỗi currency có quy tắc rounding riêng
- FX markup — platform có thể charge thêm 1-3% cho currency conversion
3.9 Fraud Detection — Phát hiện gian lận
Multi-Layer Fraud Detection
flowchart TB subgraph "Layer 1: Rules Engine" R1[Velocity Check<br/>5+ transactions trong 1 phút?] R2[Amount Check<br/>Giao dịch > threshold?] R3[Geo Check<br/>IP ở VN nhưng card ở US?] R4[Pattern Check<br/>Nhiều card khác nhau, cùng IP?] end subgraph "Layer 2: ML Model" ML1[Feature Extraction<br/>Transaction features] ML2[Scoring Model<br/>Fraud probability 0-1] ML3[Threshold Decision<br/>Score > 0.8 → block] end subgraph "Layer 3: Manual Review" MR[Human Review Queue<br/>Score 0.5-0.8] end subgraph "Decision" PASS[APPROVE] BLOCK[BLOCK + Alert] REVIEW[MANUAL REVIEW] end R1 --> |Pass| ML1 R2 --> |Pass| ML1 R3 --> |Pass| ML1 R4 --> |Pass| ML1 R1 --> |Fail| BLOCK R2 --> |Fail| BLOCK R3 --> |Fail| BLOCK R4 --> |Fail| BLOCK ML1 --> ML2 ML2 --> ML3 ML3 --> |Score < 0.5| PASS ML3 --> |Score 0.5-0.8| REVIEW ML3 --> |Score > 0.8| BLOCK REVIEW --> MR MR --> PASS MR --> BLOCK
Fraud Detection Methods
| Method | Mô tả | Ví dụ |
|---|---|---|
| Velocity checks | Đếm số giao dịch trong time window | > 5 transactions trong 1 phút → suspicious |
| Device fingerprinting | Xác định device dựa trên browser/device attributes | Cùng device thử 10 card khác nhau → fraud |
| Geo-IP analysis | So sánh IP location với billing address | IP ở Nigeria, card ở Norway → suspicious |
| BIN analysis | Kiểm tra Bank Identification Number (6 digit đầu) | BIN thuộc bank thường bị fraud |
| Address Verification (AVS) | So sánh billing address với card-on-file address | Mismatch → suspicious |
| 3D Secure | Buyer verify với bank (SMS OTP, biometric) | Liability shift cho merchant |
| Behavioral analysis | Phân tích hành vi user trên site | Copy-paste card number (thay vì gõ) → bot |
| ML model | Supervised learning trên historical fraud data | Feature: amount, time, location, device, velocity |
Capacity Estimation — Ước lượng năng lực
Assumptions
| Thông số | Giá trị | Giải thích |
|---|---|---|
| Transactions/day | 1,000,000 (1M) | Quy mô Shopify-like |
| Average transaction amount | $50 (USD) | Mix of small & large payments |
| Peak-to-average ratio | 5x | Black Friday, flash sales |
| Ledger entries per transaction | 4 | 2 debit + 2 credit (platform fee) |
| Average payload size (payment record) | 2 KB | JSON with metadata |
| Average ledger entry size | 500 bytes | Compact, structured |
| Wallet update per transaction | 1 | Credit merchant wallet |
| PSP webhook retry | 3 attempts | Standard retry policy |
QPS Calculation
Nhận xét: 58 TPS peak — đây là moderate scale. Không cần sharding phức tạp cho payment service. Nhưng ledger writes cao hơn vì mỗi transaction tạo nhiều entries.
Ledger Storage
Nhận xét: 3.65 TB cho 5 năm ledger — vừa đủ cho single PostgreSQL instance với partitioning. Nhưng vì ledger là append-only và immutable, có thể archive entries cũ hơn 1 năm sang cold storage (S3/Glacier).
Payment Records Storage
Wallet Database Sizing
Nhận xét: Wallet DB rất nhỏ (20 MB) vì chỉ lưu current balance. Nhưng write throughput là vấn đề chính — 58 TPS peak vào wallet với locking.
Wallet Write Throughput
Nhận xét: Nếu 1 merchant nhận 100+ payments/s (ví dụ: Shopee flash sale), pessimistic locking trên single row sẽ bottleneck. Đây là giả định serialization hoàn toàn dưới pessimistic lock — thực tế cao hơn nếu transactions được pipeline.
Giải pháp production (Stripe-style):
- Sharded balance per merchant: Chia balance thành K rows với suffix random (
balance_{merchant_id}_{shard_0..K-1}). Write phân tán → throughput tăng K lần. Đọc tổng = SUM tất cả shards.- Write-ahead buffer + async aggregation: Ghi vào append-only event log → aggregate balance bất đồng bộ. Throughput không bị giới hạn bởi lock contention.
- Coalescing/batching: Nhiều update cùng merchant trong cùng tick (e.g., 10ms window) được merge thành 1 update — giảm số write thật.
- Optimistic locking: Dùng version column thay vì pessimistic lock; conflict thì retry. Phù hợp khi conflict rate thấp.
Tham chiếu: Stripe Engineering — Online migrations at scale và Idempotency keys.
Tóm tắt Estimation
| Metric | Value |
|---|---|
| Transaction QPS (peak) | ~58/s |
| Ledger write QPS (peak) | ~232/s |
| Webhook QPS (peak) | ~87/s |
| Ledger storage/year | ~730 GB |
| Payment records/year | ~730 GB |
| Wallet DB size | ~20 MB |
| Total storage/year | ~1.5 TB |
| 5-year retention | ~7.5 TB |
Security — Bảo mật
PCI-DSS Compliance (Payment Card Industry Data Security Standard)
PCI-DSS là bộ tiêu chuẩn bảo mật bắt buộc cho mọi tổ chức xử lý, lưu trữ, hoặc truyền tải card data. Có 4 levels:
| Level | Điều kiện | Yêu cầu Audit |
|---|---|---|
| Level 1 | > 6M transactions/year | Annual on-site audit bởi QSA |
| Level 2 | 1M - 6M transactions/year | Annual SAQ + quarterly scan |
| Level 3 | 20K - 1M transactions/year | Annual SAQ + quarterly scan |
| Level 4 | < 20K transactions/year | Annual SAQ |
Với 1M transactions/day = 365M/year → Level 1. Chi phí audit: 500K/năm. Đây là lý do chính để dùng PSP — giảm PCI-DSS scope.
PCI-DSS 12 Requirements (Tóm tắt)
| # | Requirement | Áp dụng cho Payment System |
|---|---|---|
| 1 | Install and maintain firewall | Network segmentation, WAF |
| 2 | Change default passwords | Harden mọi service |
| 3 | Protect stored cardholder data | KHÔNG lưu card number — dùng tokenization |
| 4 | Encrypt transmission of cardholder data | TLS 1.2+ cho mọi communication |
| 5 | Use and update anti-virus | Endpoint protection |
| 6 | Develop secure systems | Secure SDLC, code review, SAST/DAST |
| 7 | Restrict access to cardholder data | RBAC, principle of least privilege |
| 8 | Assign unique IDs to each person | Individual accounts, no shared credentials |
| 9 | Restrict physical access | Data center security |
| 10 | Track and monitor all access | Audit logging — tham chiếu Tuan-14-AuthN-AuthZ-Security |
| 11 | Test security systems regularly | Penetration testing, vulnerability scanning |
| 12 | Maintain information security policy | Documentation, training |
Tokenization — Không bao giờ lưu Card Number
Flow an toàn:
1. Buyer nhập card trên PSP's hosted page (KHÔNG phải server của em)
2. PSP tokenize: 4242-4242-4242-4242 → tok_abc123xyz
3. Em chỉ lưu token + last 4 digits (4242) + card brand (Visa)
4. Khi charge lại: gửi token cho PSP, PSP lookup card từ vault
Flow KHÔNG an toàn (vi phạm PCI-DSS):
1. Buyer nhập card trên form của em
2. Card number đi qua server của em → EM PHẢI COMPLY PCI-DSS LEVEL 1
3. Em lưu card number trong DB → THẢM HỌA nếu bị hack
Rule tuyệt đối: Card number KHÔNG BAO GIỜ touch server của em. Không transit, không store, không process. Dùng hosted payment page hoặc client-side tokenization (Stripe Elements).
Encryption — Mã hóa
| Layer | Type | Standard | Áp dụng |
|---|---|---|---|
| In transit | TLS 1.2+ | AES-256-GCM | Mọi API call, webhook, internal service communication |
| At rest | AES-256 | FIPS 140-2 | Database encryption, backup encryption |
| Application level | Field-level encryption | AES-256 + KMS | PII (tên, email, địa chỉ), token |
| Key management | KMS (Key Management Service) | HSM-backed | AWS KMS, HashiCorp Vault |
AML (Anti-Money Laundering) — Chống rửa tiền
| Check | Mô tả | Threshold |
|---|---|---|
| KYC (Know Your Customer) | Verify danh tính merchant khi onboard | Bắt buộc cho mọi merchant |
| Transaction monitoring | Phát hiện pattern bất thường | Nhiều giao dịch nhỏ (structuring) |
| Sanctions screening | Kiểm tra tên trong danh sách cấm vận | OFAC, EU sanctions list |
| Suspicious Activity Report (SAR) | Báo cáo giao dịch đáng ngờ cho cơ quan chức năng | > $10,000 USD (US) hoặc pattern bất thường |
| PEP screening | Kiểm tra Politically Exposed Persons | Government officials, relatives |
3D Secure (3DS)
3D Secure là protocol xác thực thêm một layer giữa buyer và issuing bank:
1. Buyer enter card details
2. PSP detect card enrolled in 3DS
3. Redirect to bank's 3DS page
4. Buyer verify: SMS OTP / Biometric / App notification
5. Bank confirm → PSP proceed with payment
Lợi ích:
- Liability shift: Nếu fraud xảy ra với 3DS-authenticated transaction, bank chịu trách nhiệm (không phải merchant)
- Reduce chargebacks: Buyer đã xác thực → khó dispute
- SCA compliance: Strong Customer Authentication (EU PSD2 requirement)
Audit Logging
Mọi giao dịch phải có audit trail — tham chiếu Tuan-14-AuthN-AuthZ-Security:
| Field | Mô tả |
|---|---|
event_id | UUID unique cho mỗi event |
timestamp | ISO 8601 với timezone |
actor | User/service thực hiện action |
action | CREATE_PAYMENT, APPROVE_REFUND, UPDATE_STATUS, etc. |
resource | payment_id, transaction_id |
old_value | Giá trị trước khi thay đổi |
new_value | Giá trị sau khi thay đổi |
ip_address | IP của actor |
user_agent | Browser/client info |
reason | Lý do thay đổi (bắt buộc cho manual actions) |
Rule: Audit log là append-only, immutable, lưu riêng biệt với application DB. Không ai (kể cả admin) có thể xóa audit log. Lưu tối thiểu 7 năm (regulatory requirement).
DevOps & Monitoring — Vận hành và giám sát
Key Metrics to Monitor
| Metric | Mô tả | Alert Threshold | Severity |
|---|---|---|---|
| Payment Success Rate | % giao dịch thành công | < 95% → alert | P1 (Critical) |
| Payment Latency P99 | Thời gian xử lý 99th percentile | > 3s → alert | P2 (High) |
| Payment Latency P50 | Median latency | > 500ms → investigate | P3 (Medium) |
| PSP Error Rate | % request tới PSP bị lỗi | > 5% → alert | P1 |
| Webhook Processing Lag | Độ trễ xử lý webhook | > 5 minutes → alert | P2 |
| Reconciliation Discrepancy | Số giao dịch không khớp | > 0.1% → alert | P1 |
| DLQ Depth | Số message trong Dead Letter Queue | > 10 → alert | P2 |
| Wallet Balance Anomaly | Balance thay đổi bất thường | Negative balance → alert | P1 |
| Fraud Block Rate | % giao dịch bị fraud detection block | > 10% → investigate | P3 |
| Idempotency Hit Rate | % request trùng idempotency key | > 5% → investigate (client bug?) | P3 |
Dashboard Layout
| Panel | Metrics | Visualize |
|---|---|---|
| Payment Health | Success rate, error rate, volume | Time series (last 24h) |
| Latency | P50, P95, P99 per endpoint | Heatmap |
| Money Flow | Total pay-in, pay-out, net | Counter + trend |
| PSP Status | Per-PSP success rate, latency | Table + sparkline |
| Reconciliation | Match %, discrepancy count | Daily bar chart |
| Fraud | Block rate, ML score distribution | Histogram |
PCI-DSS Audit Logging Requirements
| Yêu cầu | Implementation |
|---|---|
| Log mọi access tới cardholder data | Audit log cho mọi token lookup |
| Log mọi admin action | RBAC + audit trail cho admin panel |
| Log authentication attempts | Success + failure, lockout after 5 failures |
| Centralized logging | ELK Stack hoặc Splunk, SIEM integration |
| Log integrity | Write-once storage, tamper-evident (hash chain) |
| Retention | Minimum 1 năm online, 7 năm total |
| Daily log review | Automated anomaly detection + manual review |
Disaster Recovery
| Scenario | RTO (Recovery Time Objective) | RPO (Recovery Point Objective) | Strategy |
|---|---|---|---|
| Single service failure | < 30s | 0 (no data loss) | Auto-restart, health check, replica |
| Database failure | < 5 min | 0 | Hot standby, synchronous replication |
| AZ (Availability Zone) failure | < 5 min | 0 | Multi-AZ deployment |
| Region failure | < 30 min | < 1 min | Cross-region replica, DNS failover |
| PSP outage | < 1 min | 0 | Multi-PSP failover (Stripe → Adyen) |
| Complete data center loss | < 1 hour | < 5 min | Cross-region backup + restore |
Multi-PSP strategy: Không bao giờ phụ thuộc vào 1 PSP. Khi Stripe outage (đã xảy ra nhiều lần), auto-failover sang Adyen hoặc PayPal. Cần abstract PSP interface.
Deployment Strategy
| Strategy | Mô tả | Risk |
|---|---|---|
| Blue-Green | 2 identical environments, switch traffic | Rollback instant nhưng tốn 2x resources |
| Canary | Route 1-5% traffic sang new version, monitor | Phát hiện bug sớm, rollback nhanh |
| Feature flags | Toggle new payment features per merchant | Granular control |
| Database migration | Always backward compatible, no breaking changes | Zero-downtime migration |
Payment system deployment rule: KHÔNG BAO GIỜ deploy payment changes vào Friday afternoon hoặc trước major sale events (Black Friday, 11.11). Luôn deploy vào đầu tuần, giờ thấp điểm.
Mermaid Diagrams — Tổng hợp
Diagram 1: Payment Flow End-to-End
flowchart LR B[Buyer] -->|1. Pay $50| FE[Frontend] FE -->|2. POST /payments| PS[Payment Service] PS -->|3. Check fraud| FD[Fraud Detection] FD -->|4. Pass| PS PS -->|5. Create charge| PSP[PSP - Stripe] PSP -->|6. Authorize| CN[Card Network - Visa] CN -->|7. Auth request| IB[Issuing Bank] IB -->|8. Approved| CN CN -->|9. Approved| PSP PSP -->|10. Webhook: success| PS PS -->|11. Record| LEDGER[Ledger] PS -->|12. Credit| WALLET[Wallet] PS -->|13. Notify| NOTIF[Notification] NOTIF -->|14. Email| B style PS fill:#1e88e5,color:#fff style PSP fill:#43a047,color:#fff style LEDGER fill:#e53935,color:#fff style WALLET fill:#ff6f00,color:#fff
Diagram 2: Double-Entry Ledger Flow
flowchart TB subgraph "Transaction: Buyer pays Merchant $50" T1[Transaction ID: txn_001] end subgraph "Ledger Entries" E1[Entry 1: DEBIT<br/>Account: Buyer Cash<br/>Amount: +$50.00] E2[Entry 2: CREDIT<br/>Account: Platform Holding<br/>Amount: +$50.00] end subgraph "Settlement: Platform pays Merchant" T2[Transaction ID: txn_002] end subgraph "Settlement Entries" E3[Entry 3: DEBIT<br/>Account: Platform Holding<br/>Amount: -$50.00] E4[Entry 4: CREDIT<br/>Account: Merchant Revenue<br/>Amount: +$48.55] E5[Entry 5: CREDIT<br/>Account: Platform Fee<br/>Amount: +$1.45] end subgraph "Validation" V1{Debit = Credit?<br/>$50 = $50} V2{Debit = Credit?<br/>$50 = $48.55 + $1.45} end T1 --> E1 T1 --> E2 E1 --> V1 E2 --> V1 T2 --> E3 T2 --> E4 T2 --> E5 E3 --> V2 E4 --> V2 E5 --> V2 style E1 fill:#e53935,color:#fff style E3 fill:#e53935,color:#fff style E2 fill:#43a047,color:#fff style E4 fill:#43a047,color:#fff style E5 fill:#43a047,color:#fff
Diagram 3: Reconciliation Pipeline
flowchart TB subgraph "Sources T+0" A[Internal Ledger<br/>Real-time writes] end subgraph "Sources T+1" B[PSP Settlement File<br/>CSV/JSON daily export] C[Bank Statement<br/>MT940 / API] end subgraph "ETL Pipeline" E1[Extract<br/>Fetch files from SFTP/API] E2[Transform<br/>Normalize format, currency] E3[Load<br/>Insert into reconciliation DB] end subgraph "Matching Engine" M1[Match by transaction_id] M2[Fuzzy match by amount + date] M3[Manual match for exceptions] end subgraph "Results" R1[MATCHED<br/>96-99% of transactions] R2[TIMING DIFF<br/>1-3%, auto-resolve next day] R3[DISCREPANCY<br/>< 0.1%, needs investigation] end subgraph "Actions" ACT1[Auto-close matched] ACT2[Re-run tomorrow] ACT3[Create JIRA ticket<br/>Alert on-call] end A --> E1 B --> E1 C --> E1 E1 --> E2 E2 --> E3 E3 --> M1 M1 --> M2 M2 --> M3 M1 --> R1 M2 --> R2 M3 --> R3 R1 --> ACT1 R2 --> ACT2 R3 --> ACT3 style R1 fill:#43a047,color:#fff style R2 fill:#ffa726,color:#000 style R3 fill:#e53935,color:#fff
Diagram 4: Payment State Machine (Simplified)
stateDiagram-v2 [*] --> CREATED CREATED --> PROCESSING: Submit to PSP PROCESSING --> AUTHORIZED: Bank approved PROCESSING --> FAILED: Bank declined AUTHORIZED --> CAPTURED: Capture funds AUTHORIZED --> VOIDED: Cancel payment CAPTURED --> SETTLED: Settlement complete CAPTURED --> REFUND_PENDING: Refund requested REFUND_PENDING --> REFUNDED: Refund processed FAILED --> [*] VOIDED --> [*] SETTLED --> [*] REFUNDED --> [*]
Aha Moments — Khoảnh khắc “A ha!”
#1 — Idempotency is THE most important concept: Trong payment system, mọi thứ có thể fail — network timeout, service crash, DB down. Idempotency key đảm bảo rằng dù retry bao nhiêu lần, kết quả vẫn giống nhau. Nếu em chỉ nhớ được 1 concept từ bài này, hãy nhớ idempotency. Stripe, PayPal, mọi PSP lớn đều xây dựng trên nền tảng này.
#2 — Never store card numbers: Đây không phải recommendation — đây là luật. Vi phạm PCI-DSS Requirement 3 = phạt 100,000/tháng + mất quyền xử lý card + reputational damage không thể phục hồi. Luôn dùng tokenization qua PSP.
#3 — Reconciliation catches everything: Dù code có tốt đến đâu, sẽ luôn có edge case: network blip, timezone bug, race condition, PSP error. Reconciliation là safety net cuối cùng. Trong thế giới fintech, câu nói nổi tiếng là: “Trust, but verify”. Reconciliation chính là “verify”.
#4 — Payment is a state machine, not a function call: Nhiều junior dev nghĩ payment đơn giản:
chargeCard(amount) → success/fail. Thực tế, payment đi qua 10+ states, có thể stuck ở bất kỳ state nào, và cần compensation logic cho mỗi failure point. State machine giúp em biết chính xác payment đang ở đâu và nên làm gì tiếp.
#5 — Double-entry là self-checking mechanism: Kế toán dùng double-entry từ 500 năm trước vì một lý do đơn giản: nếu tổng debit khác tổng credit, chắc chắn có lỗi. Trong software, đây là built-in invariant check. Mỗi khi ledger imbalanced → trigger alert → investigate ngay.
#6 — Money has no “undo” button: Khác với database (
ROLLBACK), tiền thật không có undo. Khi charge card → tiền đã chuyển. Muốn hoàn → phải tạo refund transaction riêng, mất 3-10 ngày làm việc. Đây là lý do payment system cần cẩn thận gấp 10 lần so với hệ thống khác.
#7 — Floating-point is the enemy of money:
0.1 + 0.2 != 0.3trong hầu hết ngôn ngữ lập trình. Lưu tiền bằng float → sai sót tích lũy → reconciliation fail → audit finding. Luôn dùng integer (cents/đồng) hoặc Decimal type.
Common Pitfalls — Sai lầm thường gặp
Pitfall 1: Không có Idempotency Key
Sai:
POST /payments {amount: 500000}— mỗi request tạo payment mới Đúng:POST /payments {amount: 500000, idempotency_key: "uuid-abc-123"}— retry an toàn
Hậu quả: Buyer bị charge 2 lần khi network timeout + retry. Phải refund manually → mất thời gian + uy tín.
Pitfall 2: Lưu Card Number trong DB
Sai:
INSERT INTO payments (card_number, amount) VALUES ('4242424242424242', 500000)Đúng:INSERT INTO payments (card_token, last_four, amount) VALUES ('tok_abc', '4242', 500000)
Hậu quả: Vi phạm PCI-DSS → phạt + mất license. Nếu DB bị hack → hàng triệu card bị lộ → class action lawsuit.
Pitfall 3: Dùng Float cho tiền
Sai:
amount FLOAT→ 49.99 + 0.01 có thể = 50.000000001 Đúng:amount BIGINT→ lưu 4999 (cents), display $49.99
Hậu quả: Reconciliation luôn sai lệch vài cent. Tích lũy hàng triệu giao dịch → sai lệch hàng nghìn đô.
Pitfall 4: Không handle Payment “in limbo”
Sai: Timeout → assume failed → không retry → buyer đã bị charge nhưng không nhận hàng Đúng: Timeout → check payment status via PSP API → retry with idempotency key nếu cần
Hậu quả: Tiền “biến mất” — buyer mất tiền, merchant không nhận được. Chỉ reconciliation mới catch được.
Pitfall 5: Cho phép Update/Delete Ledger Entry
Sai:
UPDATE ledger SET amount = 400000 WHERE id = 123— sửa entry cũ Đúng: Tạo reversal entry mới:INSERT INTO ledger (type, amount) VALUES ('REVERSAL', -500000)
Hậu quả: Mất audit trail. Auditor không thể verify lịch sử giao dịch. Compliance violation.
Pitfall 6: Single PSP dependency
Sai: Chỉ tích hợp Stripe. Stripe down → toàn bộ payment down. Đúng: Abstract PSP interface, tích hợp ít nhất 2 PSPs. Auto-failover khi 1 PSP down.
Hậu quả: Stripe outage (đã xảy ra nhiều lần trong lịch sử) = 100% revenue loss trong thời gian outage.
Pitfall 7: Không có Reconciliation
Sai: “Code của mình đúng rồi, không cần đối soát.” Đúng: Daily reconciliation là bắt buộc cho mọi payment system.
Hậu quả: Sau 6 tháng phát hiện bug gây sai lệch 50,000 giao dịch. Không biết bắt đầu fix từ đâu. Regulatory audit fail.
Pitfall 8: Deploy payment changes vào Friday
Sai: Deploy new payment flow vào Friday 5pm → bug → weekend → nobody available to fix Đúng: Deploy đầu tuần, giờ thấp điểm, canary 1% traffic trước
Hậu quả: Hàng nghìn giao dịch fail trong weekend. On-call engineer scramble fix trong khi thiếu context.
Internal Links — Liên kết nội bộ
| Topic | Link | Liên quan thế nào |
|---|---|---|
| Authentication & Security | Tuan-14-AuthN-AuthZ-Security | API authentication, RBAC cho admin panel, JWT cho service-to-service |
| Data Security & Encryption | Tuan-15-Data-Security-Encryption | Encryption at rest/in transit, tokenization, KMS |
| Microservices Pattern | Tuan-11-Microservices-Pattern | Saga pattern, service decomposition, event-driven architecture |
| Database Sharding & Replication | Tuan-07-Database-Sharding-Replication | Ledger partitioning, wallet DB replication, read replicas |
| Message Queue | Tuan-08-Message-Queue | Webhook processing, DLQ, async notification |
| Monitoring & Observability | Tuan-13-Monitoring-Observability | Payment metrics, alerting, distributed tracing |
| Back-of-the-envelope | Tuan-02-Back-of-the-envelope | Estimation methodology |
Tổng kết — Summary
Payment System Architecture Principles
| Principle | Giải thích |
|---|---|
| Idempotency everywhere | Mọi write operation phải idempotent |
| Never store sensitive data | Card number, CVV → tokenization via PSP |
| Append-only ledger | Không update, không delete, chỉ append |
| State machine for payment | Explicit states + transitions, không implicit |
| Reconciliation as safety net | Daily đối soát, bắt mọi sai lệch |
| Multi-PSP resilience | Không phụ thuộc 1 PSP |
| Integer for money | BIGINT in smallest unit, no floating-point |
| Audit everything | Mọi action có trail, lưu 7+ năm |
| Defense in depth | Multiple layers: fraud, 3DS, AML, reconciliation |
| Fail safe, not fail fast | Payment in limbo → check status → retry safely |
Khi nào em gặp lại Payment System?
- E-commerce platform: Shopee, Tiki, Lazada
- SaaS billing: Subscription management, usage-based billing
- Marketplace: Grab, Uber, Airbnb (3-party payment: buyer → platform → seller)
- Fintech: Digital wallet, P2P transfer, lending platform
- Interview: Amazon, Stripe, PayPal, Shopify — đây là bài system design phổ biến
“Hieu, payment system là nơi mà mọi concept em đã học — distributed systems, database, security, monitoring — hội tụ lại. Hiểu payment system = hiểu cách tiền chảy trong internet. Và tiền chảy chính xác = niềm tin của hàng triệu người dùng.”
Tài liệu này dựa trên Chapter 7: Payment System từ “System Design Interview Volume 2” của Alex Xu, được mở rộng và bổ sung cho context của Hieu (Backend Dev transitioning to System Architect).