Machine Coding Problem

Multi-Currency Wallet

macoAllfintechfx-rate-servicerounding
Commonly Asked By:StripePayPalWiseAirbnb

Requirements & Scope

Functional Requirements

  • Multi-Holding Wallet Balance: Holds concurrent balance registers for USD, EUR, GBP, and JPY in a single customer entity.
  • Exchange Conversion Quote Locks: Offers dynamic lock-in pricing quotes with TTL protection bounds before conversion execution.
  • Precision Rounding Safeguards: Utilizes Banker's Rounding (Round-Half-To-Even) to prevent floating-point calculation drift.
  • Cross-Currency Settlements: Debits sending wallets in sending assets, calculates conversion rates with fees, and credits targets in foreign denominations atomically.

Fintech FX Architecture

Relationship blueprint for FX services, locked quotes, and atomic transfers:

Loading...

Design Principles

  • Factory Pattern (FX Quotes): Encapsulates currency quote creation with randomized tokens, expiration timestamps, and markup configurations.
  • Value Object Pattern: Models currency values as immutable BigDecimal-style wrappers to safeguard against representation defects.

Settlement Workflow

Conversion & Settlement Sequence

  1. Quote Generation: Check raw mid-market prices. Apply spread fee (e.g. 1.5% markup) to produce final net rate and seal under 10-second lock.
  2. User Validation: Confirm transfer amount does not exceed available sender balances.
  3. Deduction & Calculation: Deduct source wallet currency balance. Convert target values using the net rate: Target = Source * Rate * (1 - Spread).
  4. Bankers' Rounding: Apply banker's rounding rules (round half to nearest even digit) to 4 decimal places.
  5. Credit Target: Credit target wallet's secondary holdings register.

Clean Code Blueprint

Production reference implementations demonstrating currency holdings maps, Banker's rounding calculations, and cross-currency FX flows:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.*;

enum CurrencyCode {
    USD, EUR, GBP, JPY
}

class CurrencyBalance {
    private final String walletId;
    private final Map<CurrencyCode, BigDecimal> balances = new ConcurrentHashMap<>();

    public CurrencyBalance(String walletId) {
        this.walletId = walletId;
        for (CurrencyCode code : CurrencyCode.values()) {
            balances.put(code, BigDecimal.ZERO.setScale(4, RoundingMode.HALF_EVEN));
        }
    }

    public String getWalletId() { return walletId; }

    public BigDecimal getBalance(CurrencyCode code) {
        return balances.getOrDefault(code, BigDecimal.ZERO);
    }

    public void credit(CurrencyCode code, BigDecimal amount) {
        balances.merge(code, amount.setScale(4, RoundingMode.HALF_EVEN), BigDecimal::add);
    }

    public void debit(CurrencyCode code, BigDecimal amount) {
        BigDecimal current = getBalance(code);
        if (current.compareTo(amount) < 0) {
            throw new IllegalArgumentException("Insufficient funds in " + code);
        }
        balances.put(code, current.subtract(amount).setScale(4, RoundingMode.HALF_EVEN));
    }

    public Map<CurrencyCode, BigDecimal> getAllBalances() {
        return Collections.unmodifiableMap(balances);
    }
}

class FXQuote {
    private final String quoteId;
    private final CurrencyCode from;
    private final CurrencyCode to;
    private final BigDecimal rate;
    private final BigDecimal fee;
    private final long expiresAt;

    public FXQuote(CurrencyCode from, CurrencyCode to, BigDecimal rate, BigDecimal fee, long ttlMs) {
        this.quoteId = UUID.randomUUID().toString();
        this.from = from;
        this.to = to;
        this.rate = rate;
        this.fee = fee;
        this.expiresAt = System.currentTimeMillis() + ttlMs;
    }

    public String getQuoteId() { return quoteId; }
    public CurrencyCode getFrom() { return from; }
    public CurrencyCode getTo() { return to; }
    public BigDecimal getRate() { return rate; }
    public BigDecimal getFee() { return fee; }
    public boolean isExpired() { return System.currentTimeMillis() > expiresAt; }
}

class FXRateService {
    private final Map<String, BigDecimal> exchangeRates = new ConcurrentHashMap<>();
    private final double spreadMarkup = 0.015; // 1.5% transaction spread fee

    public FXRateService() {
        exchangeRates.put("USD_EUR", new BigDecimal("0.9250"));
        exchangeRates.put("USD_GBP", new BigDecimal("0.7880"));
        exchangeRates.put("USD_JPY", new BigDecimal("155.60"));
        
        exchangeRates.put("EUR_USD", new BigDecimal("1.0810"));
        exchangeRates.put("GBP_USD", new BigDecimal("1.2690"));
        exchangeRates.put("JPY_USD", new BigDecimal("0.0064"));
    }

    public BigDecimal getMidMarketRate(CurrencyCode from, CurrencyCode to) {
        if (from == to) return BigDecimal.ONE;
        String pair = from.name() + "_" + to.name();
        BigDecimal rate = exchangeRates.get(pair);
        if (rate == null) {
            BigDecimal fromUSD = exchangeRates.get(from.name() + "_USD");
            BigDecimal toUSD = exchangeRates.get("USD_" + to.name());
            if (fromUSD != null && toUSD != null) {
                return fromUSD.multiply(toUSD).setScale(6, RoundingMode.HALF_EVEN);
            }
            throw new IllegalArgumentException("Unsupported currency pair: " + pair);
        }
        return rate;
    }

    public FXQuote generateQuote(CurrencyCode from, CurrencyCode to, long ttlMs) {
        BigDecimal midRate = getMidMarketRate(from, to);
        BigDecimal feeRate = new BigDecimal(String.valueOf(spreadMarkup));
        return new FXQuote(from, to, midRate, feeRate, ttlMs);
    }
}

class ConversionService {
    public BigDecimal convert(BigDecimal sourceAmount, FXQuote quote) {
        if (quote.isExpired()) {
            throw new IllegalStateException("FX quote has expired");
        }
        
        BigDecimal grossTarget = sourceAmount.multiply(quote.getRate());
        BigDecimal feeDeduction = grossTarget.multiply(quote.getFee());
        BigDecimal netTarget = grossTarget.subtract(feeDeduction);

        return netTarget.setScale(4, RoundingMode.HALF_EVEN);
    }
}

class CrossCurrencyTransfer {
    private final FXRateService fxRateService;
    private final ConversionService conversionService;

    public CrossCurrencyTransfer(FXRateService fxRateService, ConversionService conversionService) {
        this.fxRateService = fxRateService;
        this.conversionService = conversionService;
    }

    public synchronized boolean executeTransfer(CurrencyBalance sender, CurrencyBalance receiver, 
                                                 CurrencyCode sendCurrency, CurrencyCode recvCurrency, 
                                                 BigDecimal sendAmount) {
        FXQuote quote = fxRateService.generateQuote(sendCurrency, recvCurrency, 10000);
        BigDecimal recvAmount = conversionService.convert(sendAmount, quote);

        try {
            sender.debit(sendCurrency, sendAmount);
            receiver.credit(recvCurrency, recvAmount);
            
            logTransfer(sender.getWalletId(), receiver.getWalletId(), sendAmount, sendCurrency, recvAmount, recvCurrency, quote);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private void logTransfer(String from, String to, BigDecimal sAmt, CurrencyCode sCur, 
                             BigDecimal rAmt, CurrencyCode rCur, FXQuote quote) {
        System.out.printf("FX TRANSFER: Sent %s %s -> Recv %s %s [Rate: %s, Fee spread: %s]%n", 
                sAmt, sCur, rAmt, rCur, quote.getRate(), quote.getFee());
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== MULTI-CURRENCY WALLET DEMO (JAVA) ===");

        FXRateService fxRateService = new FXRateService();
        ConversionService conversionService = new ConversionService();
        CrossCurrencyTransfer transferService = new CrossCurrencyTransfer(fxRateService, conversionService);

        CurrencyBalance alice = new CurrencyBalance("W_ALICE");
        CurrencyBalance bob = new CurrencyBalance("W_BOB");

        alice.credit(CurrencyCode.USD, new BigDecimal("500.0000"));
        bob.credit(CurrencyCode.EUR, new BigDecimal("100.0000"));

        System.out.println("Initial Alice USD: $" + alice.getBalance(CurrencyCode.USD));
        System.out.println("Initial Bob EUR: โ‚ฌ" + bob.getBalance(CurrencyCode.EUR));

        System.out.println("\n--- Performing cross-currency transfer of 100 USD to EUR ---");
        boolean success = transferService.executeTransfer(alice, bob, CurrencyCode.USD, CurrencyCode.EUR, new BigDecimal("100.0000"));
        System.out.println("Transfer Success: " + success);

        System.out.println("\nAlice Balances after transfer:");
        alice.getAllBalances().forEach((k, v) -> {
            if (v.compareTo(BigDecimal.ZERO) > 0) {
                System.out.println(k + ": " + v);
            }
        });

        System.out.println("\nBob Balances after transfer:");
        bob.getAllBalances().forEach((k, v) -> {
            if (v.compareTo(BigDecimal.ZERO) > 0) {
                System.out.println(k + ": " + v);
            }
        });
    }
}

Interactive Simulator

โ– Precise Multi-Currency Wallet Visualizer

Locked-in dynamic quotes, bankers' rounding (round-half-even) decimals, and automated markup fee spread calculators.

๐Ÿ“Š Live Exchange Conversion Rates Board

Adjust Markup spread:1.5%
USD/EUR
0.9250
Net: 0.9111
USD/GBP
0.7880
Net: 0.7762
USD/JPY
155.6000
Net: 153.2660
EUR/GBP
0.8520
Net: 0.8392
GBP/JPY
197.4000
Net: 194.4390

๐Ÿ‘ค Alice's Multi-Asset Holdings

USD
500.0000
EUR
120.0000
GBP
0.0000
JPY
15000.0000

๐Ÿ‘ค Bob's Multi-Asset Holdings

USD
200.0000
EUR
350.5000
GBP
80.0000
JPY
0.0000

๐Ÿ’ธ Execute Cross-Currency Transfer (Alice โ”€โ”€โ–บ Bob)

๐Ÿ’ฑ Locked quote FX converter

๐Ÿ–ณ FX Platform Daemon Logs
[13:50:00] Precise Bankers' Rounding FX registers initialized.

๐Ÿ’ฌ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...