Chuyển đến nội dung chính

レッスン 8: デジタル ウォレット アーキテクチャ - 電子ウォレットと残高管理

デジタルウォレットのアーキテクチャ: アカウントタイプ、残高管理、チャージ/引き出しフロー。ウォレットからウォレットへの送金。エスクロー口座と保留残高。

🏗️ アーキテクチャ — レッスン 8 レッスン 8: デジタル ウォレット アーキテクチャ - E-ウォレットと残高管理

FinTech と決済プラットフォームのアーキテクチャ

パート 3: デジタルウォレットと台帳システム

xdev.asia

レッスン 8: デジタル ウォレット アーキテクチャ - 電子ウォレットと残高管理

はじめに

デジタル ウォレットは、あらゆる 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 向けの会計エンジン設計。