Machine Coding Problem

Multi-Currency Wallet

macoAllfintechfx-rate-servicerounding
Commonly Asked By:StripePayPalWiseAirbnb

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.

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);
            }
        });
    }
}