Design an exchange rate for two currencies

Mehmet Karakose
TOM Tech
Published in
5 min readFeb 20, 2024

--

This article provides example code snippets of two-currency rate exchange, designed for high availability.

Step 1 — Understand the problem and establish design scope

Business Side

Creating a scalable system that converts millions of currencies per day.

The service costs us $0.01 for each call and it has a %98 availability rate. The daily budget to be spent for this service was determined as 5 dollars.

Example response: GBP -> EUR : 1.15

Our service method is called from outside 1000/sec. We would like to be highly available here and service majority of the customers in less than 100 ms.

Highly available => https://en.wikipedia.org/wiki/High_availability

Developer Side

— Start Info

+----------------+-----------------+-------------------+
| Availability % | Downtime per day| Downtime per year |
+----------------+-----------------+-------------------+
| 99 % | 14.40 minutes | 3.65 days |
| 99.9 % | 1.44 minutes | 8.77 hours |
| 99.99 % | 8.64 seconds | 52.60 minutes |
| 99.999 % | 864 milliseconds| 5.26 minutes |
+----------------+-----------------+-------------------+

— End Info

Firstly, the service we are forced to use is paid and has a very low availability rate. So much so that it creates interruptions amounting to approximately 176 hours per year, or 30 minutes per day. In this context, we need to focus on caching and retry/circuit breaker mechanisms.

— Start Info

Retry Pattern
Purpose
: This pattern proposes that if an operation (for example, a network request) fails due to a temporary error, it retries at a certain time interval and a certain number of times.
Usage Scenario: Ideal for temporary errors. For example, temporary network outage or short-term overload of the service.
Approach: The transaction can be tried again using a specific strategy (usually exponential backoff) to reduce the error rate and increase the chance of a successful response.

Circuit Breaker Pattern
Purpose
: This pattern aims to stop calls to a service that constantly gives errors for a certain period of time and give the system time to recover.
Usage Scenario: Ideal for persistent or recurring errors. For example, a service crash or resource shortage.
Approach: When the number of errors exceeds a certain threshold, the circuit breaker goes “on” and all calls to this service are temporarily stopped. After a certain period of time, the circuit breaker goes into the “half-open” state and a limited number of calls are made to check the status of the service. If service returns to normal, the circuit breaker goes “off.”

— End Info

And since system interruptions can be prolonged or persistent, it would be better proceed with Circuit Breaker. For now, let’s store the latest currency rate in a local cache, and not delve into implementations like Redis.

We also need a service to limit our service usage, let’s call it RateLimiter. Since we cannot exceed the daily spending limit of $5, we can only send 500 requests per day.

5/0.01 = 500 daily usage
There are 1440 minutes in a day.
1440/500 = 2.88 minutes

I will use BigDecimal for the amount and rate to be converted.

— Start Info

Rounding Control: BigDecimal offers the ability to control the rounding needs that arise as a result of calculations.
Compliance with Financial Standards: Financial practices are often subject to standards that require precision and strict adherence to rounding rules.

— End Info

Step 2 — High level design

Step 3 — Deep dive coding

CircuitBreaker

class CircuitBreaker {
private enum State {
CLOSED, OPEN, HALF_OPEN
}

private State state = State.CLOSED;
private long lastOpenedTimestamp;
private long lastAttemptTimestamp = 0;
private static final long OPEN_STATE_DURATION = TimeUnit.MINUTES.toMillis(1);
private static final long HALF_OPEN_STATE_DURATION = TimeUnit.MINUTES.toMillis(3);

public synchronized boolean isRequestAllowed() {
long currentTime = System.currentTimeMillis();
if (state == State.OPEN && currentTime - lastOpenedTimestamp > OPEN_STATE_DURATION) {
state = State.HALF_OPEN;
lastAttemptTimestamp = currentTime;
}
if (state == State.HALF_OPEN && currentTime - lastAttemptTimestamp > HALF_OPEN_STATE_DURATION) {
lastAttemptTimestamp = currentTime;
return true;
}
return state == State.CLOSED || state == State.HALF_OPEN;
}

public synchronized void openCircuit() {
if (state != State.OPEN) {
state = State.OPEN;
lastOpenedTimestamp = System.currentTimeMillis();
}
}

public synchronized void closeCircuit() {
if (state != State.CLOSED) {
state = State.CLOSED;
}
}

public synchronized void successfulRequest() {
if (state == State.HALF_OPEN) {
closeCircuit();
}
}
}

RateLimiter

class RateLimiter {
private final AtomicInteger dailyRequestCount = new AtomicInteger(0);
private final AtomicLong lastRequestTime = new AtomicLong(0);
private static final long THREE_MINUTES_IN_MILLIS = 180000;
private static final int DAILY_LIMIT = 500;
private static final long ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000;

public boolean isRequestAllowed() {
long currentTime = System.currentTimeMillis();
long lastRequest = lastRequestTime.get();

if (currentTime - lastRequest >= ONE_DAY_IN_MILLIS) {
dailyRequestCount.set(0);
}

if (currentTime - lastRequest >= THREE_MINUTES_IN_MILLIS && dailyRequestCount.get() < DAILY_LIMIT) {
lastRequestTime.set(currentTime);
dailyRequestCount.incrementAndGet();
return true;
}
return false;
}
}

Cache

class CurrencyRateCache {
private final ConcurrentHashMap<String, BigDecimal> rates = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Long> timestamps = new ConcurrentHashMap<>();

public void putRate(String currencyPair, BigDecimal rate) {
rates.put(currencyPair, rate);
timestamps.put(currencyPair, System.currentTimeMillis());
}

public BigDecimal getRate(String currencyPair) {
Long timestamp = timestamps.get(currencyPair);
if (timestamp == null || isExpired(timestamp)) {
return null;
}
return rates.get(currencyPair);
}

public BigDecimal getRateEvenIfExpired(String currencyPair) {
return rates.get(currencyPair);
}

private boolean isExpired(Long timestamp) {
return System.currentTimeMillis() - timestamp > TimeUnit.MINUTES.toMillis(5);
}
}

RateLimitService

interface RateService {
BigDecimal getRate(Currency from, Currency to);
}

class RateServiceImpl implements RateService {
private final CurrencyRateCache cache = new CurrencyRateCache();
private final CircuitBreaker circuitBreaker = new CircuitBreaker();
private final RateLimiter rateLimiter = new RateLimiter();

@Override
public BigDecimal getRate(Currency fromCurrency, Currency toCurrency) {
String currencyPair = fromCurrency.name() + toCurrency.name();
BigDecimal rate = cache.getRate(currencyPair);

if (rate == null) {
if (circuitBreaker.isRequestAllowed() && rateLimiter.isRequestAllowed()) {
try {
rate = fetchRateFromExternalService(fromCurrency, toCurrency);
cache.putRate(currencyPair, rate);
circuitBreaker.successfulRequest();
} catch (Exception e) {
circuitBreaker.openCircuit();
rate = cache.getRateEvenIfExpired(currencyPair);
}
} else {
rate = cache.getRateEvenIfExpired(currencyPair);
}
}
return rate;
}
// MOCK
private BigDecimal fetchRateFromExternalService(Currency fromCurrency, Currency toCurrency) {
return BigDecimal.valueOf(0.88);
}
}

CurrencyConverterService

public class CurrencyConverterService {
private final RateService rateService;

public CurrencyConverterService(RateService rateService) {
this.rateService = rateService;
}

public BigDecimal convert(Currency fromCurrency, Currency toCurrency, BigDecimal amount) {
BigDecimal rate = rateService.getRate(fromCurrency, toCurrency);
return rate != null ? rate.multiply(amount) : null;
}

public static void main(String[] args) {
RateService rateService = new RateServiceImpl();
CurrencyConverterService service = new CurrencyConverterService(rateService);

BigDecimal amount = BigDecimal.valueOf(100);
Currency fromCurrency = Currency.USD;
Currency toCurrency = Currency.EUR;

BigDecimal convertedAmount = service.convert(fromCurrency, toCurrency, amount);
if (convertedAmount != null) {
System.out.printf("%s %s = %s %s%n", amount.toPlainString(), fromCurrency, convertedAmount.toPlainString(), toCurrency);
} else {
System.out.println("There was a problem with the exchange rate conversion.");
}
}
}

Currency Enum

enum Currency {
TRY,
EUR,
GBP,
USD
}

--

--

Mehmet Karakose
TOM Tech

Software Engineer, Organizer, Travelpreneur ✈️ 🌍💫