
はじめに
デジタル ウォレットは、あらゆる FinTech スーパー アプリの中心です。ベトナムでは、MoMo、ZaloPay、ShopeePay が電子ウォレットを最も人気のある支払い方法に変えました。この記事では、プロフェッショナルなデジタル ウォレット プラットフォームのアーキテクチャを分析します。
1. ウォレットのアーキテクチャの概要
1.1 アカウントの種類
Wallet Account Hierarchy:
├── User Wallet
│ ├── Main Balance (available for payment)
│ ├── Hold Balance (reserved/pending)
│ ├── Bonus Balance (promotions, non-withdrawable)
│ └── Credit Balance (lending, BNPL)
│
├── Merchant Wallet
│ ├── Settlement Balance
│ ├── Fee Balance
│ └── Reserve Balance (for chargebacks)
│
├── Platform Wallet
│ ├── Fee Collection Account
│ ├── Float Account (pooled user funds)
│ └── Escrow Account
│
└── System Wallet
├── Suspense Account (unresolved transactions)
├── Reconciliation Account
└── Interest Pool Account
1.2 コアコンポーネント
┌─────────────────────────────────────────────────────────────┐
│ WALLET SERVICE │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Account │ │ Balance │ │ Transaction │ │
│ │ Manager │ │ Engine │ │ Processor │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────────┘ │
│ │ │ │ │
│ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────────┐ │
│ │ KYC │ │ Hold │ │ Transfer │ │
│ │ Validator │ │ Manager │ │ Engine │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Event Store (Kafka) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2. 残高管理
2.1 残高計算戦略
Strategy 1: Snapshot + Delta (Recommended)
─────────────────────────────────────────
Snapshot (cached): 1,000,000 VND (at timestamp T)
+ Credit: +200,000 (top-up)
+ Credit: +50,000 (cashback)
- Debit: -150,000 (payment)
─────────────────────────────
Current Balance: 1,100,000 VND
Every N transactions or periodically → create new snapshot
2.2 同時残高更新
// Optimistic locking approach
@Entity
@Table(name = "wallet_balances")
public class WalletBalance {
@Id
private UUID walletId;
@Column(name = "available_balance")
private long availableBalance; // In smallest currency unit
@Column(name = "hold_balance")
private long holdBalance;
@Version
private long version; // Optimistic lock
public void debit(long amount) {
if (availableBalance < amount) {
throw new InsufficientFundsException();
}
this.availableBalance -= amount;
}
public void credit(long amount) {
this.availableBalance += amount;
}
public void placeHold(long amount) {
if (availableBalance < amount) {
throw new InsufficientFundsException();
}
this.availableBalance -= amount;
this.holdBalance += amount;
}
public void releaseHold(long amount) {
this.holdBalance -= amount;
this.availableBalance += amount;
}
}
2.3 データベース設計
CREATE TABLE wallet_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
account_type VARCHAR(30) NOT NULL, -- 'MAIN', 'BONUS', 'CREDIT'
currency VARCHAR(3) NOT NULL DEFAULT 'VND',
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
kyc_level INT NOT NULL DEFAULT 0,
daily_limit BIGINT NOT NULL,
monthly_limit BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE wallet_balances (
wallet_id UUID PRIMARY KEY REFERENCES wallet_accounts(id),
available_balance BIGINT NOT NULL DEFAULT 0,
hold_balance BIGINT NOT NULL DEFAULT 0,
total_credited BIGINT NOT NULL DEFAULT 0,
total_debited BIGINT NOT NULL DEFAULT 0,
version BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT positive_balance CHECK (available_balance >= 0),
CONSTRAINT positive_hold CHECK (hold_balance >= 0)
);
CREATE TABLE wallet_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_id UUID NOT NULL REFERENCES wallet_accounts(id),
type VARCHAR(30) NOT NULL, -- 'TOP_UP', 'PAYMENT', 'TRANSFER', 'WITHDRAWAL'
direction VARCHAR(10) NOT NULL, -- 'CREDIT', 'DEBIT'
amount BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
reference_id UUID,
reference_type VARCHAR(50),
description TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_wallet_txn_wallet_date
ON wallet_transactions(wallet_id, created_at DESC);
3. チャージと引き出し
3.1 補充の流れ
Customer ──► Select top-up method ──► Payment Gateway
│
┌──────▼──────┐
│ Bank/Card │
│ Charge │
└──────┬──────┘
│ Success
┌──────▼──────┐
│ Credit │
│ Wallet │
└──────┬──────┘
│
┌──────▼──────┐
│ Notify │
│ Customer │
└─────────────┘
3.2 ウォレット間の転送
@Transactional
public TransferResult transfer(TransferRequest request) {
var sender = walletRepository.findWithLock(request.getSenderId());
var receiver = walletRepository.findWithLock(request.getReceiverId());
// Validate
sender.validateTransfer(request.getAmount());
limitChecker.check(sender, request.getAmount());
// Execute double-entry
sender.debit(request.getAmount());
receiver.credit(request.getAmount());
// Save both
walletRepository.save(sender);
walletRepository.save(receiver);
// Record transactions
var debitTxn = WalletTransaction.debit(sender, request.getAmount());
var creditTxn = WalletTransaction.credit(receiver, request.getAmount());
transactionRepository.saveAll(List.of(debitTxn, creditTxn));
// Publish event
eventBus.publish(new TransferCompletedEvent(request));
return TransferResult.success(debitTxn.getId());
}
4. ウォレットの制限と KYC
4.1 リミット構造 (SBV 規制による)
KYC Level 0 (chưa xác minh):
├── Số dư tối đa: 10,000,000 VND
├── Giao dịch/ngày: 5,000,000 VND
└── Giao dịch/tháng: 20,000,000 VND
KYC Level 1 (xác minh cơ bản):
├── Số dư tối đa: 50,000,000 VND
├── Giao dịch/ngày: 30,000,000 VND
└── Giao dịch/tháng: 100,000,000 VND
KYC Level 2 (xác minh đầy đủ - eKYC):
├── Số dư tối đa: 100,000,000 VND
├── Giao dịch/ngày: 100,000,000 VND
└── Giao dịch/tháng: 300,000,000 VND
概要
デジタル ウォレットは、パフォーマンス (同時アクセス) と正確性 (お金なし) のバランスをとる必要があります。
- オプティミスティックロックによるバランス更新
- すべての取引で 二重入力
- KYCレベルに応じて強制を制限
- イベント駆動による非同期処理
次の記事: 複式簿記システム — FinTech 向けの会計エンジン設計。