Design International Money Transfers
[!NOTE] This is the “Full System Design” version of Wise. It connects every pattern we’ve learned: Idempotency, Ledgers, Async Queues, and Reconciliation.
1. The Problem: Moving Value across Borders
Unlike a local bank transfer, an international transfer involves currency conversion, compliance screening (KYC/AML), and external payout rails (SWIFT, ACH, SEPA).
The Interview Challenge:
“Design a system where a user can send EUR to a friend in India receiving INR. The system must handle the rate lock, funding, conversion, and final delivery while ensuring the accounting is perfectly accurate even if the network fails.”
2. Requirements & Goals
Functional Requirements
- Transfer Lifecycle: Create, Fund, Convert, and Deliver funds.
- Status Tracking: Real-time visibility for the user.
- Audit Trail: Immutable record of every balance change.
Non-Functional Requirements
- Financial Correctness: Never credit the recipient without debiting the sender.
- Resilience: Rails (external partners) will fail. Handle retries gracefully.
- Consistency: Use Double-Entry Bookkeeping to ensure the system is audit-ready.
3. System APIs
Initiate Transfer
POST /v1/transfers
Idempotency-Key: "uuid"
{
"source_currency": "EUR",
"target_currency": "INR",
"source_amount": 10000,
"recipient_id": "r_99"
}
Response:
{
"transfer_id": "t_1",
"status": "CREATED",
"quoted_rate": 88.54,
"expires_at": "..."
}
4. High-Level Architecture: The Orchestrator Path
We use a Microservices architecture to separate concerns.
flowchart TD
U[User App] --> API[Transfer API]
API --> ORCH[Transfer Orchestrator]
ORCH -->|1. Lock Rate| FX[FX Service]
ORCH -->|2. Record Intent| L[Ledger Service]
ORCH -->|3. Accept Funds| P[Payment Ingestor]
ORCH -->|4. Dispatch Payout| RA[Rail Adapters]
RA -->|Webhook/Poll| ORCH
ORCH -->|5. Update Status| L
5. Detailed Design: The Transfer State Machine
Handling a transfer is a long-running process. It must be modeled as a Finite State Machine (FSM).
| State | Action | Financial Event |
|---|---|---|
| CREATED | User initiates. | None. |
| FUNDED | User pays via card/bank. | Debit User Wallet, Credit Wise Purgatory Account. |
| CONVERTED | FX conversion applied. | Debit EUR Purgatory, Credit INR Purgatory. |
| OUTGOING | Sent to Indian bank rail. | Debit Wise INR Account, Credit Recipient Bank. |
| SETTLED | Delivery confirmed. | Finalize audit log. |
6. Database Design: Double-Entry Ledger
CRITICAL: Do not just have a balance column in a users table. You need a Ledger.
Table: ledger_entries
| id | tx_id | account_id | amount | type | status |
|---|---|---|---|---|---|
| 1 | tx_001 | user_alice_eur | -100.00 | DEBIT | POSTED |
| 2 | tx_001 | wise_eur_holding | +100.00 | CREDIT | POSTED |
| 3 | tx_002 | wise_inr_payout | -8854.00 | DEBIT | PENDING |
| 4 | tx_002 | user_bob_inr | +8854.00 | CREDIT | PENDING |
Atomicity (Postgres)
The move from wise_eur_holding to recipient_inr must be Atomic. Use a database transaction. If one part fails, the whole thing rolls back.
7. Deep Dive: Three-Way Reconciliation
The biggest risk for Wise is “Lost Money” in the network. We solve this with Reconciliation.
flowchart LR
L[Internal Ledger] --- MATCH[Match Processor]
RA[Rail Reports] --- MATCH
BS[Bank Statements] --- MATCH
MATCH -->|Gaps Found| ALARM[Engineering Alarm]
- Level 1 (Internal): Do our Credits match our Debits at the end of every hour?
SUM(amount) == 0. - Level 2 (Rail): We told the Indian bank to pay $100. Did their nightly CSV confirm a $100 payout?
- Level 3 (Bank): Did our actual bank balance decrease by $100?
8. Summary: The Senior Interview Checklist
- Idempotency: “What if the user clicks ‘Pay’ twice?” (Use the
Idempotency-Keyand talk about thePENDINGstate). - Rail Failure: “What if the Indian bank rail is down for 6 hours?” (Talk about an Outgoing Queue with exponential backoff).
- Compensating Transactions: “What if the transfer fails at the last second?” (Explain how you’d ‘Reverse’ the ledger entries).
- Auditability: “Finance team wants to know why we paid 8854 INR instead of 8800.” (Show how the
rate_snapshot_idlinks the FX service to the Ledger).