System Design Problem

Design a Multi-Currency Payment System

Commonly Asked By:StripePayPalWiseAirbnb

Interview Setup

Interview Prompt

Design a multi-currency payment system that accepts payments in 150+ currencies, locks FX rates during checkout, and settles to merchants in their preferred currency at 10M cross-currency transactions/day.

Clarifying Questions (ask before designing)

QuestionWhy it matters
Does the platform hold FX risk between charge and settlement, or pass it to the merchant?Determines rate-lock TTL, treasury hedging, and markup strategy, which is core to the business model.
Authorize-only vs capture later? Refunds and chargebacks in scope?
  • Sets idempotency, ledger, and reconciliation boundaries
  • lock may expire between auth and capture.
Who sets rounding rules across buyer display, charge amount, and merchant settlement?
  • JPY has 0 decimals while BHD has 3
  • mismatched rounding causes penny drift across 10M txns/day.
Single PSP or multi-PSP routing by currency and region?
  • Stripe excels at USD/EUR
  • Razorpay for INR
  • routing affects approval rates and fees.

Scope

In scope

  • FX rate service
  • Settlement currency
  • Rounding rules
  • Multi-PSP routing
  • Cross-border compliance
  • Capacity estimation with shown math

Out of scope (state explicitly)

  • Fraud ML model training (covered in Fraud Detection System): a rules engine is sufficient unless explicitly requested
  • Merchant onboarding and KYC workflows
  • Building a PSP or bank from scratch

Functional Requirements

Start by confirming buyer vs merchant currency flows with your interviewer. Ask whether FX rate locking during checkout and multi-currency ledger balances are in scope for this round.

  • Accept payments in any currency: Buyer pays in their local currency (EUR, GBP, JPY, INR...)
  • Settle in merchant's currency: Convert and settle to merchant's preferred currency
  • Real-time exchange rates: Use live FX rates for conversion at payment time
  • FX rate locking: Lock exchange rate for a window (15-30 min) during checkout
  • Multi-currency wallets: Users and merchants hold balances in multiple currencies, coordinating with our Digital Wallet System architecture.
  • Cross-border transfers: Send money internationally with transparent FX fees
  • FX markup/fee: Configurable spread on exchange rates for revenue
  • Currency display: Show prices in buyer's local currency across the platform

Non-Functional Requirements

Your interviewer will care most about FX accuracy and rate-lock consistency. A locked rate must hold through checkout even if the market moves, so explicitly clarify who absorbs FX risk during the lock window.

  • Accuracy: FX rates accurate to 6 decimal places; no rounding errors
  • Low Latency: Currency conversion decision in < 50 ms
  • Consistency: Locked FX rate honored even if market moves during checkout
  • Compliance: Adhere to currency regulations per country (capital controls, sanctioned currencies)
  • Scale: 10M+ cross-currency transactions/day
  • Availability: 99.99%

Capacity Estimations

Run this math before you size the FX rate cache. Cross-currency transactions per day and supported currency pairs tell you rate-ingestion frequency; lock TTL drives Redis key volume at checkout.

MetricCalculationValue
Supported currenciesGiven150+
FX rate updatesGivenEvery 30 seconds from providers
Cross-currency transactions / dayGiven10M
FX rate lookups / secDerived from daily volume ÷ 86400 (+ peak factor)50K
Locked rate recordsGiven5M active at any time

Architecture Diagram

In the room: FX rate lock at checkout means the platform holds exposure between charge and settlement; treasury hedging serves as a staff-level follow-up.

Walk your interviewer through the checkout FX path first. FX Rate Service ingests live rates from multiple providers, Payment Router selects PSP by currency, and a per-currency ledger records every debit/credit with rate locks bridging buyer and merchant currencies at checkout. Frame rate ingestion, locking, and settlement as three distinct architectural hops.

Loading...

Component Deep Dives

Next we examine each component in the architecture, starting with FX rate management because every downstream conversion decision depends on fresh, multi-source rates.

FX Rate Management

Multi-provider rate ingestion with staleness detection is worth stating before your interviewer asks what happens when an upstream rate feed goes stale.

Rate ingestion:
  Multiple FX rate providers (redundancy + best rate selection):
    - Reuters/Bloomberg: institutional rates
    - ECB (European Central Bank): reference rates
    - Open Exchange Rates API: retail rates

Rate with markup:
  Raw rate: 1.0845
  Platform markup: 2% (revenue)
  Buy rate (user buys USD with EUR): 1.0845 * 0.98 = 1.0628
  Sell rate (user sells USD for EUR): 1.0845 * 1.02 = 1.1062

Rate triangulation:
  No direct rate for NGN->BRL?
  NGN -> USD -> BRL (via common intermediate currency)
  rate = ngn_usd_rate * usd_brl_rate

FX Rate Locking

Rate locking establishes the checkout user experience contract. Explain who absorbs FX risk during the 15-minute window before diving into the lock token flow.

Without locking, the user sees €92.17, takes 5 minutes to fill payment details, and the rate changes before completion. With locking, the platform absorbs FX risk for 15 minutes. At scale (10M transactions/day), average FX movement in 15 minutes remains < 0.05%.

Checkout flow with rate lock:

1. User sees price: "$100 USD" -> "Show in my currency" -> "€92.17 EUR"
2. User clicks "Pay €92.17":
   FX Service locks rate:
     INSERT INTO rate_locks (lock_id, from_ccy, to_ccy, rate, expires_at)
     VALUES ('lock-uuid', 'EUR', 'USD', 1.0850, NOW() + '15 min');
3. User completes payment within 15 min:
   Payment Service: validate lock_id is not expired
   Charge: €92.17 EUR -> convert at locked rate 1.0850 -> $100.00 USD to merchant
4. If user takes > 15 min:
   Lock expired -> re-quote with current rate

Multi-Currency Ledger

Per-currency ledger accounts keep FX conversions auditable, aligning with the double-entry bookkeeping detailed in our Banking Ledger System, ensuring that every cross-border payment creates balanced entries in both currencies.

Transaction: Buyer pays €92.17, Merchant receives $100.00

EUR ledger:
  Entry 1: { account: buyer_eur,     DEBIT,  €92.17 }
  Entry 2: { account: platform_eur,  CREDIT, €92.17 }

USD ledger:
  Entry 3: { account: platform_usd,  DEBIT, $100.00 }
  Entry 4: { account: merchant_usd, CREDIT, $100.00 }

FX conversion record:
  { from: €92.17 EUR, to: $100.00 USD, rate: 1.0850, lock_id: "lock-uuid" }

Per-currency balancing:
  SUM(EUR debits) = SUM(EUR credits) ?
  SUM(USD debits) = SUM(USD credits) ?

Smallest Currency Units

Different currencies have different decimal places:
  USD: 2 decimals ($1.00 = 100 cents)
  JPY: 0 decimals (¥100 = 100 yen, no fractional yen)
  BHD: 3 decimals (0.001 BHD = 1 fils)

Best practice: store amounts as integers in smallest unit.
  $100.00 -> 10000 (cents)
  ¥100 -> 100
  0.500 BHD -> 500 (fils)

Avoids all decimal arithmetic issues. Integer math is exact.

API Design

HTTP
GET /api/v1/fx/quote?from=EUR&to=USD&amount=100
-> { "from": "EUR", "to": "USD", "amount": 100.00,
     "converted": 106.28, "rate": 1.0845, "markup": 0.02,
     "effective_rate": 1.0628, "valid_for_seconds": 30 }

POST /api/v1/fx/lock
{ "from_currency": "EUR", "to_currency": "USD", "rate": 1.0850, "ttl_seconds": 900 }
-> { "lock_id": "lock-uuid", "expires_at": "2026-03-14T11:15:00Z" }

POST /api/v1/payments/cross-currency
{ "buyer_currency": "EUR", "buyer_amount": 92.17,
  "merchant_currency": "USD", "merchant_amount": 100.00,
  "fx_lock_id": "lock-uuid", "merchant_id": "m-uuid" }
-> { "payment_id": "pay-uuid", "status": "completed",
     "fx_rate_used": 1.0850, "fx_fee": 1.84 }

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
402 Payment Required: account balance or payment method has insufficient funds
502 Bad Gateway: payment gateway provider timeout, poll transaction status endpoint

Data Model

Redis: Live Rates

fx_rate:{base}:{quote}  -> Hash { rate, bid, ask, provider, updated_at }
TTL: 60 (stale after 1 min without update)

PostgreSQL: Locks & Ledger

SQL
CREATE TABLE rate_locks (
    lock_id UUID PRIMARY KEY, from_currency CHAR(3), to_currency CHAR(3),
    locked_rate DECIMAL(12,6), expires_at TIMESTAMPTZ, used BOOLEAN DEFAULT FALSE,
    payment_id UUID, created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE fx_conversions (
    conversion_id UUID PRIMARY KEY, payment_id UUID,
    from_currency CHAR(3), from_amount DECIMAL(18,2),
    to_currency CHAR(3), to_amount DECIMAL(18,2),
    rate_used DECIMAL(12,6), lock_id UUID,
    platform_fee DECIMAL(18,2), created_at TIMESTAMPTZ
);

Event Bus Design (Kafka)

Topic: payment-events
  Partitions: 64
  Partition key: payment_id
  Retention: 7 years (FX audit trail)

Producer: Payment Service after multi-currency charge + ledger posting
  Event: { payment_id, buyer_currency, seller_currency, fx_rate, fx_lock_id, amount_local, amount_settlement }

Consumer groups:
  1. notification: receipt with FX breakdown
  2. fx-risk-engine: monitor net exposure per currency; auto-hedge if > $1M
  3. settlement: daily batch payouts in local currencies

Sync path: FX quote + lock in Redis; charge via PSP < 500ms
Async path: settlement and hedging never block payment authorization
DLQ: payment-events-dlq

Fault Tolerance

ConcernSolution
FX provider down
  • Multiple providers with fallback
  • use last known rate with staleness warning
Rate lock expired mid-payment
  • Extend lock by 5 min on active payment
  • or re-quote
Rounding errors
  • Use DECIMAL(18,6) for rates
  • banker's rounding
FX risk
  • Platform treasury team hedges FX exposure daily
  • automated rebalancing
Sanctioned currenciesCountry/currency blocklist enforced at API gateway level

Rate Staleness and Provider Failover

Multi-provider strategy:
  Priority order: Reuters (best quality) -> Bloomberg -> ECB -> OpenExchangeRates
  
  Every 30 seconds:
    1. Try primary provider (Reuters)
    2. If fails: fall to Bloomberg
    3. If both fail: use ECB (updated hourly)
    4. If all fail: use last known rate with staleness flag
  
  Staleness rules:
    Rate < 1 min old: "live" (green)
    Rate 1-5 min old: "recent" (yellow)
    Rate 5-30 min old: "stale" (orange): show warning
    Rate > 30 min old: "expired" (red)

Additional Considerations

Interview Walkthrough

  • 25-minute cut

    Skip arch50/arch75 depth unless staff.

    • FX quote → lock → charge → settle flow (8 min)
    • Minor units as integer cents rather than floating point numbers (9 min)
    • Rate lock at authorize protects buyer and merchant (8 min)
  • Frame the problem as FX risk management rather than basic currency conversion, because the platform holds exposure between charge and settlement.
  • Walk through checkout: lock FX rate at payment initiation (15-min window), charge in buyer currency, settle to merchant in their currency.
  • Explain rate sourcing hierarchy: primary provider → secondary fallback → last-known-good with staleness flag and wider spread.
  • Cover treasury hedging: net daily FX exposure per currency pair and execute forward contracts to limit platform P&L swings.
  • Mention payment routing: route USD through Stripe US, EUR through Adyen EU, local methods (UPI, PIX) through regional acquirers.
  • Discuss multi-currency ledger with separate balance accounts per currency and cross-currency transfers via FX conversion entries, coordinating with the Banking Ledger System and Digital Wallet System.
  • Orchestrate multi-step cross-currency payments and merchant payouts using the saga pattern detailed in Distributed Transactions: 2PC vs Saga.
  • Common pitfall: using live mid-market rates without locking at checkout, because a 2% currency move between cart and capture creates merchant disputes.

Settlement and Treasury

Platform accumulates various currencies throughout the day:
  EUR balance: +€500K, USD balance: -$542K, GBP balance: +£100K

Daily settlement:
  1. Calculate net position per currency
  2. Execute FX trades in wholesale market (better rates than retail)
  3. Transfer settled amounts to merchants' bank accounts
  4. T+1 or T+2 settlement (1-2 business days)

FX hedging:
  If platform knows it will receive €1M tomorrow (from European sales):
  Buy a EUR/USD forward contract today -> lock in today's rate
  Eliminates FX risk on the $1M settlement.

Payment Routing by Currency

Different PSPs have different strengths per currency/country:
  Stripe: excellent for USD, EUR, GBP
  Adyen: strong in EU and Asia, 150+ currencies
  Local PSPs: Razorpay (INR), iDEAL (NL), Alipay (CNY)

Smart routing:
  For INR payments: route to Razorpay (lower fees, higher approval rates)
  For EUR payments: route to Adyen (native EUR processing)
  For USD payments: route to Stripe (lowest processing fee)

Benefits: higher approval rates, lower fees, fewer chargebacks

Engineering Trade-offs

FX Risk Management: Platform Exposure

Multi-currency checkout balances FX spread revenue against buyer trust, governed by rate-lock windows and platform hedging policy.

Problem: Platform locks EUR/USD at 1.0850 for 15 min for buyer.
  If EUR/USD moves to 1.0750, platform loses on conversion.
  At 10M cross-currency txns/day x avg $50 = $500M daily volume

Mitigation:
  1. Markup covers most risk: 2% markup >> average 15-min FX movement (0.05%)
  2. Auto-hedging: if net position in any currency exceeds $1M, execute FX hedge
  3. Dynamic lock TTL: normal=15min, high volatility=5min, extreme=2min+larger markup

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...