The Currency Layer API provides real-time and historical exchange rates for 168 world currencies. This guide covers comprehensive integration with the Currency Layer API in Java applications.
Core API Client Implementation
Step 1: Basic API Client Setup
package com.example.currencylayer;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
// Core API response models
public class CurrencyLayerModels {
public static class ApiResponse {
private boolean success;
private ErrorInfo error;
private Map<String, String> terms;
private Map<String, String> privacy;
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public ErrorInfo getError() { return error; }
public void setError(ErrorInfo error) { this.error = error; }
public Map<String, String> getTerms() { return terms; }
public void setTerms(Map<String, String> terms) { this.terms = terms; }
public Map<String, String> getPrivacy() { return privacy; }
public void setPrivacy(Map<String, String> privacy) { this.privacy = privacy; }
}
public static class ErrorInfo {
private int code;
private String info;
private String type;
// Getters and setters
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getInfo() { return info; }
public void setInfo(String info) { this.info = info; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
@Override
public String toString() {
return String.format("Error %d: %s (%s)", code, info, type);
}
}
public static class LiveRatesResponse extends ApiResponse {
private String source;
private Map<String, BigDecimal> quotes;
private Long timestamp;
// Getters and setters
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public Map<String, BigDecimal> getQuotes() { return quotes; }
public void setQuotes(Map<String, BigDecimal> quotes) { this.quotes = quotes; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
}
public static class HistoricalRatesResponse extends ApiResponse {
private String source;
private LocalDate date;
private Map<String, BigDecimal> quotes;
private Long timestamp;
// Getters and setters
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public LocalDate getDate() { return date; }
public void setDate(LocalDate date) { this.date = date; }
public Map<String, BigDecimal> getQuotes() { return quotes; }
public void setQuotes(Map<String, BigDecimal> quotes) { this.quotes = quotes; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
}
public static class CurrencyConversionResponse extends ApiResponse {
private String source;
private Map<String, BigDecimal> quotes;
private BigDecimal result;
// Getters and setters
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public Map<String, BigDecimal> getQuotes() { return quotes; }
public void setQuotes(Map<String, BigDecimal> quotes) { this.quotes = quotes; }
public BigDecimal getResult() { return result; }
public void setResult(BigDecimal result) { this.result = result; }
}
public static class TimeFrameResponse extends ApiResponse {
private String source;
private LocalDate startDate;
private LocalDate endDate;
private Map<LocalDate, Map<String, BigDecimal>> quotes;
// Getters and setters
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public LocalDate getStartDate() { return startDate; }
public void setStartDate(LocalDate startDate) { this.startDate = startDate; }
public LocalDate getEndDate() { return endDate; }
public void setEndDate(LocalDate endDate) { this.endDate = endDate; }
public Map<LocalDate, Map<String, BigDecimal>> getQuotes() { return quotes; }
public void setQuotes(Map<LocalDate, Map<String, BigDecimal>> quotes) { this.quotes = quotes; }
}
public static class SupportedCurrenciesResponse extends ApiResponse {
private Map<String, CurrencyInfo> currencies;
// Getters and setters
public Map<String, CurrencyInfo> getCurrencies() { return currencies; }
public void setCurrencies(Map<String, CurrencyInfo> currencies) { this.currencies = currencies; }
}
public static class CurrencyInfo {
private String symbol;
private String name;
private String symbolNative;
private int decimalDigits;
private int rounding;
private String code;
private String namePlural;
// Getters and setters
public String getSymbol() { return symbol; }
public void setSymbol(String symbol) { this.symbol = symbol; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSymbolNative() { return symbolNative; }
public void setSymbolNative(String symbolNative) { this.symbolNative = symbolNative; }
public int getDecimalDigits() { return decimalDigits; }
public void setDecimalDigits(int decimalDigits) { this.decimalDigits = decimalDigits; }
public int getRounding() { return rounding; }
public void setRounding(int rounding) { this.rounding = rounding; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getNamePlural() { return namePlural; }
public void setNamePlural(String namePlural) { this.namePlural = namePlural; }
}
}
// Core API client
public class CurrencyLayerClient {
private final String apiKey;
private final String baseUrl;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private final boolean useHttps;
public CurrencyLayerClient(String apiKey) {
this(apiKey, "api.currencylayer.com", true);
}
public CurrencyLayerClient(String apiKey, String baseUrl, boolean useHttps) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.useHttps = useHttps;
this.objectMapper = new ObjectMapper();
this.httpClient = createHttpClient();
}
private OkHttpClient createHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
}
private String buildUrl(String endpoint, Map<String, String> params) {
String protocol = useHttps ? "https" : "http";
StringBuilder urlBuilder = new StringBuilder()
.append(protocol).append("://").append(baseUrl).append("/").append(endpoint);
Map<String, String> allParams = new HashMap<>();
allParams.put("access_key", apiKey);
if (params != null) {
allParams.putAll(params);
}
if (!allParams.isEmpty()) {
urlBuilder.append("?");
allParams.forEach((key, value) ->
urlBuilder.append(key).append("=").append(value).append("&"));
// Remove trailing &
urlBuilder.setLength(urlBuilder.length() - 1);
}
return urlBuilder.toString();
}
private <T> T executeRequest(String url, Class<T> responseType) throws CurrencyLayerException {
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", "Java-CurrencyLayer-Client/1.0")
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new CurrencyLayerException("HTTP error: " + response.code() + " - " + response.message());
}
ResponseBody body = response.body();
if (body == null) {
throw new CurrencyLayerException("Empty response body");
}
String responseBody = body.string();
T result = objectMapper.readValue(responseBody, responseType);
// Check for API-level errors
if (result instanceof CurrencyLayerModels.ApiResponse) {
CurrencyLayerModels.ApiResponse apiResponse = (CurrencyLayerModels.ApiResponse) result;
if (!apiResponse.isSuccess() && apiResponse.getError() != null) {
throw new CurrencyLayerException(apiResponse.getError().toString());
}
}
return result;
} catch (IOException e) {
throw new CurrencyLayerException("Request failed: " + e.getMessage(), e);
}
}
private <T> CompletableFuture<T> executeRequestAsync(String url, Class<T> responseType) {
return CompletableFuture.supplyAsync(() -> {
try {
return executeRequest(url, responseType);
} catch (CurrencyLayerException e) {
throw new RuntimeException(e);
}
});
}
}
Step 2: Core API Operations
// Continue CurrencyLayerClient class with API methods
public class CurrencyLayerClient {
// ... previous code ...
// 1. Get real-time exchange rates
public CurrencyLayerModels.LiveRatesResponse getLiveRates() throws CurrencyLayerException {
return getLiveRates(null, null);
}
public CurrencyLayerModels.LiveRatesResponse getLiveRates(String source, List<String> currencies)
throws CurrencyLayerException {
Map<String, String> params = new HashMap<>();
if (source != null && !source.trim().isEmpty()) {
params.put("source", source);
}
if (currencies != null && !currencies.isEmpty()) {
params.put("currencies", String.join(",", currencies));
}
String url = buildUrl("live", params);
return executeRequest(url, CurrencyLayerModels.LiveRatesResponse.class);
}
public CompletableFuture<CurrencyLayerModels.LiveRatesResponse> getLiveRatesAsync() {
return getLiveRatesAsync(null, null);
}
public CompletableFuture<CurrencyLayerModels.LiveRatesResponse> getLiveRatesAsync(
String source, List<String> currencies) {
Map<String, String> params = new HashMap<>();
if (source != null && !source.trim().isEmpty()) {
params.put("source", source);
}
if (currencies != null && !currencies.isEmpty()) {
params.put("currencies", String.join(",", currencies));
}
String url = buildUrl("live", params);
return executeRequestAsync(url, CurrencyLayerModels.LiveRatesResponse.class);
}
// 2. Get historical exchange rates
public CurrencyLayerModels.HistoricalRatesResponse getHistoricalRates(LocalDate date)
throws CurrencyLayerException {
return getHistoricalRates(date, null, null);
}
public CurrencyLayerModels.HistoricalRatesResponse getHistoricalRates(
LocalDate date, String source, List<String> currencies) throws CurrencyLayerException {
Map<String, String> params = new HashMap<>();
params.put("date", date.format(DateTimeFormatter.ISO_DATE));
if (source != null && !source.trim().isEmpty()) {
params.put("source", source);
}
if (currencies != null && !currencies.isEmpty()) {
params.put("currencies", String.join(",", currencies));
}
String url = buildUrl("historical", params);
return executeRequest(url, CurrencyLayerModels.HistoricalRatesResponse.class);
}
// 3. Currency conversion
public CurrencyLayerModels.CurrencyConversionResponse convertCurrency(
String from, String to, BigDecimal amount) throws CurrencyLayerException {
return convertCurrency(from, to, amount, null);
}
public CurrencyLayerModels.CurrencyConversionResponse convertCurrency(
String from, String to, BigDecimal amount, LocalDate date) throws CurrencyLayerException {
Map<String, String> params = new HashMap<>();
params.put("from", from);
params.put("to", to);
params.put("amount", amount.toString());
if (date != null) {
params.put("date", date.format(DateTimeFormatter.ISO_DATE));
}
String url = buildUrl("convert", params);
return executeRequest(url, CurrencyLayerModels.CurrencyConversionResponse.class);
}
// 4. Time-frame data
public CurrencyLayerModels.TimeFrameResponse getTimeFrameData(
LocalDate startDate, LocalDate endDate) throws CurrencyLayerException {
return getTimeFrameData(startDate, endDate, null, null);
}
public CurrencyLayerModels.TimeFrameResponse getTimeFrameData(
LocalDate startDate, LocalDate endDate, String source, List<String> currencies)
throws CurrencyLayerException {
Map<String, String> params = new HashMap<>();
params.put("start_date", startDate.format(DateTimeFormatter.ISO_DATE));
params.put("end_date", endDate.format(DateTimeFormatter.ISO_DATE));
if (source != null && !source.trim().isEmpty()) {
params.put("source", source);
}
if (currencies != null && !currencies.isEmpty()) {
params.put("currencies", String.join(",", currencies));
}
String url = buildUrl("timeframe", params);
return executeRequest(url, CurrencyLayerModels.TimeFrameResponse.class);
}
// 5. Get supported currencies
public CurrencyLayerModels.SupportedCurrenciesResponse getSupportedCurrencies()
throws CurrencyLayerException {
String url = buildUrl("list", null);
return executeRequest(url, CurrencyLayerModels.SupportedCurrenciesResponse.class);
}
// 6. Get currency information
public Map<String, CurrencyLayerModels.CurrencyInfo> getCurrencyInfo() throws CurrencyLayerException {
CurrencyLayerModels.SupportedCurrenciesResponse response = getSupportedCurrencies();
return response.getCurrencies();
}
}
// Custom exception
class CurrencyLayerException extends Exception {
public CurrencyLayerException(String message) {
super(message);
}
public CurrencyLayerException(String message, Throwable cause) {
super(message, cause);
}
}
Advanced Features and Caching
Step 3: Caching and Rate Limiting
package com.example.currencylayer.advanced;
import com.example.currencylayer.CurrencyLayerClient;
import com.example.currencylayer.CurrencyLayerException;
import com.example.currencylayer.CurrencyLayerModels;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
public class AdvancedCurrencyService {
private final CurrencyLayerClient client;
private final Map<String, CachedRates> ratesCache;
private final ScheduledExecutorService scheduler;
private final long cacheTTLMinutes;
public AdvancedCurrencyService(CurrencyLayerClient client) {
this(client, 10); // 10 minutes TTL by default
}
public AdvancedCurrencyService(CurrencyLayerClient client, long cacheTTLMinutes) {
this.client = client;
this.cacheTTLMinutes = cacheTTLMinutes;
this.ratesCache = new ConcurrentHashMap<>();
this.scheduler = Executors.newScheduledThreadPool(1);
// Schedule cache cleanup
scheduler.scheduleAtFixedRate(this::cleanupCache, 1, 1, TimeUnit.MINUTES);
}
private static class CachedRates {
private final Map<String, BigDecimal> rates;
private final LocalDateTime timestamp;
private final String source;
public CachedRates(Map<String, BigDecimal> rates, String source) {
this.rates = rates;
this.source = source;
this.timestamp = LocalDateTime.now();
}
public boolean isExpired(long ttlMinutes) {
return LocalDateTime.now().isAfter(timestamp.plusMinutes(ttlMinutes));
}
public Map<String, BigDecimal> getRates() { return rates; }
public String getSource() { return source; }
public LocalDateTime getTimestamp() { return timestamp; }
}
// Enhanced live rates with caching
public Map<String, BigDecimal> getCachedLiveRates(String source, List<String> currencies)
throws CurrencyLayerException {
String cacheKey = buildCacheKey("live", source, currencies);
CachedRates cached = ratesCache.get(cacheKey);
if (cached != null && !cached.isExpired(cacheTTLMinutes)) {
return new HashMap<>(cached.getRates());
}
// Fetch fresh data
CurrencyLayerModels.LiveRatesResponse response = client.getLiveRates(source, currencies);
Map<String, BigDecimal> rates = response.getQuotes();
// Update cache
ratesCache.put(cacheKey, new CachedRates(rates, source));
return rates;
}
// Bulk conversion with caching
public Map<String, BigDecimal> bulkConvert(String fromCurrency, List<String> toCurrencies,
BigDecimal amount) throws CurrencyLayerException {
Map<String, BigDecimal> results = new HashMap<>();
Map<String, BigDecimal> rates = getCachedLiveRates(fromCurrency, toCurrencies);
for (String toCurrency : toCurrencies) {
String rateKey = fromCurrency + toCurrency;
BigDecimal rate = rates.get(rateKey);
if (rate != null) {
results.put(toCurrency, amount.multiply(rate));
}
}
return results;
}
// Historical analysis
public Map<String, List<BigDecimal>> getHistoricalTrend(String currency, LocalDate startDate,
LocalDate endDate, String baseCurrency)
throws CurrencyLayerException {
CurrencyLayerModels.TimeFrameResponse response =
client.getTimeFrameData(startDate, endDate, baseCurrency, Arrays.asList(currency));
Map<String, List<BigDecimal>> trends = new HashMap<>();
List<BigDecimal> rates = new ArrayList<>();
response.getQuotes().entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
String rateKey = baseCurrency + currency;
BigDecimal rate = entry.getValue().get(rateKey);
if (rate != null) {
rates.add(rate);
}
});
trends.put(currency, rates);
return trends;
}
// Rate change calculation
public Map<String, RateChange> calculateRateChanges(LocalDate fromDate, LocalDate toDate,
List<String> currencies, String baseCurrency)
throws CurrencyLayerException {
CurrencyLayerModels.HistoricalRatesResponse fromRates =
client.getHistoricalRates(fromDate, baseCurrency, currencies);
CurrencyLayerModels.HistoricalRatesResponse toRates =
client.getHistoricalRates(toDate, baseCurrency, currencies);
Map<String, RateChange> changes = new HashMap<>();
for (String currency : currencies) {
String rateKey = baseCurrency + currency;
BigDecimal fromRate = fromRates.getQuotes().get(rateKey);
BigDecimal toRate = toRates.getQuotes().get(rateKey);
if (fromRate != null && toRate != null) {
BigDecimal change = toRate.subtract(fromRate);
BigDecimal percentChange = change.divide(fromRate, 6, BigDecimal.ROUND_HALF_UP)
.multiply(BigDecimal.valueOf(100));
changes.put(currency, new RateChange(fromRate, toRate, change, percentChange));
}
}
return changes;
}
// Currency strength analysis
public Map<String, CurrencyStrength> analyzeCurrencyStrength(List<String> currencies,
int daysBack) throws CurrencyLayerException {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusDays(daysBack);
Map<String, CurrencyStrength> strengths = new HashMap<>();
for (String currency : currencies) {
Map<String, List<BigDecimal>> trends = getHistoricalTrend(currency, startDate, endDate, "USD");
List<BigDecimal> rates = trends.get(currency);
if (rates != null && rates.size() >= 2) {
BigDecimal startRate = rates.get(0);
BigDecimal endRate = rates.get(rates.size() - 1);
BigDecimal change = endRate.subtract(startRate);
BigDecimal percentChange = change.divide(startRate, 6, BigDecimal.ROUND_HALF_UP)
.multiply(BigDecimal.valueOf(100));
BigDecimal volatility = calculateVolatility(rates);
strengths.put(currency, new CurrencyStrength(currency, percentChange, volatility));
}
}
return strengths;
}
private BigDecimal calculateVolatility(List<BigDecimal> rates) {
if (rates.size() < 2) return BigDecimal.ZERO;
// Calculate standard deviation as volatility measure
BigDecimal mean = rates.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(rates.size()), 6, BigDecimal.ROUND_HALF_UP);
BigDecimal variance = rates.stream()
.map(rate -> rate.subtract(mean).pow(2))
.reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(rates.size()), 6, BigDecimal.ROUND_HALF_UP);
return new BigDecimal(Math.sqrt(variance.doubleValue()));
}
private String buildCacheKey(String endpoint, String source, List<String> currencies) {
StringBuilder key = new StringBuilder(endpoint);
if (source != null) key.append(":").append(source);
if (currencies != null) key.append(":").append(String.join(",", currencies));
return key.toString();
}
private void cleanupCache() {
ratesCache.entrySet().removeIf(entry ->
entry.getValue().isExpired(cacheTTLMinutes));
}
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Supporting data classes
public static class RateChange {
private final BigDecimal fromRate;
private final BigDecimal toRate;
private final BigDecimal absoluteChange;
private final BigDecimal percentChange;
public RateChange(BigDecimal fromRate, BigDecimal toRate,
BigDecimal absoluteChange, BigDecimal percentChange) {
this.fromRate = fromRate;
this.toRate = toRate;
this.absoluteChange = absoluteChange;
this.percentChange = percentChange;
}
// Getters
public BigDecimal getFromRate() { return fromRate; }
public BigDecimal getToRate() { return toRate; }
public BigDecimal getAbsoluteChange() { return absoluteChange; }
public BigDecimal getPercentChange() { return percentChange; }
}
public static class CurrencyStrength {
private final String currency;
private final BigDecimal performance;
private final BigDecimal volatility;
public CurrencyStrength(String currency, BigDecimal performance, BigDecimal volatility) {
this.currency = currency;
this.performance = performance;
this.volatility = volatility;
}
// Getters
public String getCurrency() { return currency; }
public BigDecimal getPerformance() { return performance; }
public BigDecimal getVolatility() { return volatility; }
public BigDecimal getRiskAdjustedReturn() {
if (volatility.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return performance.divide(volatility, 6, BigDecimal.ROUND_HALF_UP);
}
}
}
Spring Boot Integration
Step 4: Spring Boot Configuration and Service
package com.example.currencylayer.spring;
import com.example.currencylayer.CurrencyLayerClient;
import com.example.currencylayer.CurrencyLayerException;
import com.example.currencylayer.CurrencyLayerModels;
import com.example.currencylayer.advanced.AdvancedCurrencyService;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@Configuration
@EnableAsync
public class CurrencyLayerConfig {
@Bean
@ConfigurationProperties(prefix = "currencylayer")
public CurrencyLayerProperties currencyLayerProperties() {
return new CurrencyLayerProperties();
}
@Bean
public CurrencyLayerClient currencyLayerClient(CurrencyLayerProperties properties) {
return new CurrencyLayerClient(
properties.getApiKey(),
properties.getBaseUrl(),
properties.isUseHttps()
);
}
@Bean
public AdvancedCurrencyService advancedCurrencyService(CurrencyLayerClient client) {
return new AdvancedCurrencyService(client, 15); // 15 minutes cache TTL
}
}
@ConfigurationProperties(prefix = "currencylayer")
class CurrencyLayerProperties {
private String apiKey;
private String baseUrl = "api.currencylayer.com";
private boolean useHttps = true;
private long cacheTtlMinutes = 15;
// Getters and setters
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public boolean isUseHttps() { return useHttps; }
public void setUseHttps(boolean useHttps) { this.useHttps = useHttps; }
public long getCacheTtlMinutes() { return cacheTtlMinutes; }
public void setCacheTtlMinutes(long cacheTtlMinutes) { this.cacheTtlMinutes = cacheTtlMinutes; }
}
@Service
public class CurrencyLayerService {
private final CurrencyLayerClient client;
private final AdvancedCurrencyService advancedService;
private final Map<String, CurrencyLayerModels.LiveRatesResponse> lastRates = new HashMap<>();
public CurrencyLayerService(CurrencyLayerClient client, AdvancedCurrencyService advancedService) {
this.client = client;
this.advancedService = advancedService;
}
// Scheduled task to refresh rates periodically
@Scheduled(fixedRate = 600000) // Every 10 minutes
public void refreshLiveRates() {
try {
CurrencyLayerModels.LiveRatesResponse rates = client.getLiveRates();
lastRates.put("USD", rates); // Cache USD-based rates
System.out.println("Refreshed currency rates at: " + new Date());
} catch (CurrencyLayerException e) {
System.err.println("Failed to refresh rates: " + e.getMessage());
}
}
public CurrencyLayerModels.LiveRatesResponse getCurrentRates() throws CurrencyLayerException {
if (lastRates.containsKey("USD")) {
return lastRates.get("USD");
}
return client.getLiveRates();
}
@Async
public CompletableFuture<CurrencyLayerModels.LiveRatesResponse> getCurrentRatesAsync() {
return CompletableFuture.supplyAsync(() -> {
try {
return getCurrentRates();
} catch (CurrencyLayerException e) {
throw new RuntimeException(e);
}
});
}
public BigDecimal convertCurrency(String from, String to, BigDecimal amount)
throws CurrencyLayerException {
CurrencyLayerModels.CurrencyConversionResponse response =
client.convertCurrency(from, to, amount);
return response.getResult();
}
@Async
public CompletableFuture<BigDecimal> convertCurrencyAsync(String from, String to, BigDecimal amount) {
return CompletableFuture.supplyAsync(() -> {
try {
return convertCurrency(from, to, amount);
} catch (CurrencyLayerException e) {
throw new RuntimeException(e);
}
});
}
public Map<String, BigDecimal> getRateHistory(String currency, LocalDate start, LocalDate end)
throws CurrencyLayerException {
CurrencyLayerModels.TimeFrameResponse response =
client.getTimeFrameData(start, end, "USD", Arrays.asList(currency));
Map<String, BigDecimal> history = new TreeMap<>();
response.getQuotes().forEach((date, rates) -> {
String rateKey = "USD" + currency;
BigDecimal rate = rates.get(rateKey);
if (rate != null) {
history.put(date.toString(), rate);
}
});
return history;
}
public Map<String, CurrencyLayerModels.CurrencyInfo> getAvailableCurrencies()
throws CurrencyLayerException {
return client.getCurrencyInfo();
}
// Business logic: Detect arbitrage opportunities
public List<ArbitrageOpportunity> findArbitrageOpportunities(List<String> currencies)
throws CurrencyLayerException {
List<ArbitrageOpportunity> opportunities = new ArrayList<>();
Map<String, BigDecimal> rates = advancedService.getCachedLiveRates("USD", currencies);
for (String fromCurrency : currencies) {
for (String toCurrency : currencies) {
if (!fromCurrency.equals(toCurrency)) {
String directRateKey = fromCurrency + toCurrency;
BigDecimal directRate = rates.get(directRateKey);
if (directRate != null) {
// Check triangular arbitrage
for (String viaCurrency : currencies) {
if (!viaCurrency.equals(fromCurrency) && !viaCurrency.equals(toCurrency)) {
BigDecimal arbitrageProfit = calculateTriangularArbitrage(
fromCurrency, toCurrency, viaCurrency, rates);
if (arbitrageProfit.compareTo(BigDecimal.valueOf(0.01)) > 0) {
opportunities.add(new ArbitrageOpportunity(
fromCurrency, toCurrency, viaCurrency, arbitrageProfit));
}
}
}
}
}
}
}
opportunities.sort(Comparator.comparing(ArbitrageOpportunity::getProfit).reversed());
return opportunities;
}
private BigDecimal calculateTriangularArbitrage(String from, String to, String via,
Map<String, BigDecimal> rates) {
try {
BigDecimal rate1 = rates.get(from + via); // from -> via
BigDecimal rate2 = rates.get(via + to); // via -> to
BigDecimal rate3 = rates.get(from + to); // from -> to (direct)
if (rate1 != null && rate2 != null && rate3 != null) {
BigDecimal triangularRate = rate1.multiply(rate2);
BigDecimal difference = triangularRate.subtract(rate3);
return difference.divide(rate3, 6, BigDecimal.ROUND_HALF_UP)
.multiply(BigDecimal.valueOf(100)); // as percentage
}
} catch (Exception e) {
// Ignore calculation errors
}
return BigDecimal.ZERO;
}
public static class ArbitrageOpportunity {
private final String fromCurrency;
private final String toCurrency;
private final String viaCurrency;
private final BigDecimal profit;
public ArbitrageOpportunity(String fromCurrency, String toCurrency,
String viaCurrency, BigDecimal profit) {
this.fromCurrency = fromCurrency;
this.toCurrency = toCurrency;
this.viaCurrency = viaCurrency;
this.profit = profit;
}
// Getters
public String getFromCurrency() { return fromCurrency; }
public String getToCurrency() { return toCurrency; }
public String getViaCurrency() { return viaCurrency; }
public BigDecimal getProfit() { return profit; }
@Override
public String toString() {
return String.format("Arbitrage: %s -> %s -> %s | Profit: %.2f%%",
fromCurrency, viaCurrency, toCurrency, profit);
}
}
}
REST API Controller
Step 5: Spring REST Controller
package com.example.currencylayer.controller;
import com.example.currencylayer.CurrencyLayerException;
import com.example.currencylayer.CurrencyLayerModels;
import com.example.currencylayer.spring.CurrencyLayerService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/currency")
@CrossOrigin(origins = "*")
public class CurrencyLayerController {
private final CurrencyLayerService currencyService;
public CurrencyLayerController(CurrencyLayerService currencyService) {
this.currencyService = currencyService;
}
@GetMapping("/rates/live")
public ResponseEntity<?> getLiveRates(
@RequestParam(required = false) String source,
@RequestParam(required = false) List<String> currencies) {
try {
CurrencyLayerModels.LiveRatesResponse rates = currencyService.getCurrentRates();
return ResponseEntity.ok(rates);
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
@GetMapping("/rates/historical")
public ResponseEntity<?> getHistoricalRates(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestParam(required = false) String source,
@RequestParam(required = false) List<String> currencies) {
try {
CurrencyLayerModels.HistoricalRatesResponse rates =
currencyService.getHistoricalRates(date, source, currencies);
return ResponseEntity.ok(rates);
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
@GetMapping("/convert")
public ResponseEntity<?> convertCurrency(
@RequestParam String from,
@RequestParam String to,
@RequestParam BigDecimal amount,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
try {
BigDecimal result;
if (date != null) {
result = currencyService.convertCurrency(from, to, amount, date);
} else {
result = currencyService.convertCurrency(from, to, amount);
}
return ResponseEntity.ok(Map.of(
"from", from,
"to", to,
"amount", amount,
"result", result
));
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
@GetMapping("/currencies")
public ResponseEntity<?> getSupportedCurrencies() {
try {
Map<String, CurrencyLayerModels.CurrencyInfo> currencies =
currencyService.getAvailableCurrencies();
return ResponseEntity.ok(currencies);
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
@GetMapping("/analysis/history")
public ResponseEntity<?> getRateHistory(
@RequestParam String currency,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate start,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate end) {
try {
Map<String, BigDecimal> history =
currencyService.getRateHistory(currency, start, end);
return ResponseEntity.ok(history);
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
@GetMapping("/analysis/arbitrage")
public ResponseEntity<?> findArbitrageOpportunities(
@RequestParam(required = false) List<String> currencies) {
try {
List<CurrencyLayerService.ArbitrageOpportunity> opportunities =
currencyService.findArbitrageOpportunities(currencies);
return ResponseEntity.ok(opportunities);
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
@GetMapping("/analysis/strength")
public ResponseEntity<?> analyzeCurrencyStrength(
@RequestParam List<String> currencies,
@RequestParam(defaultValue = "30") int days) {
try {
Map<String, AdvancedCurrencyService.CurrencyStrength> strengths =
currencyService.analyzeCurrencyStrength(currencies, days);
return ResponseEntity.ok(strengths);
} catch (CurrencyLayerException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage()));
}
}
// Async endpoints
@GetMapping("/rates/live/async")
public CompletableFuture<ResponseEntity<?>> getLiveRatesAsync() {
return currencyService.getCurrentRatesAsync()
.thenApply(ResponseEntity::ok)
.exceptionally(e -> ResponseEntity.badRequest()
.body(Map.of("error", e.getMessage())));
}
@GetMapping("/convert/async")
public CompletableFuture<ResponseEntity<?>> convertCurrencyAsync(
@RequestParam String from,
@RequestParam String to,
@RequestParam BigDecimal amount) {
return currencyService.convertCurrencyAsync(from, to, amount)
.thenApply(result -> ResponseEntity.ok(Map.of(
"from", from,
"to", to,
"amount", amount,
"result", result
)))
.exceptionally(e -> ResponseEntity.badRequest()
.body(Map.of("error", e.getMessage())));
}
}
Testing and Error Handling
Step 6: Comprehensive Testing
package com.example.currencylayer.test;
import com.example.currencylayer.CurrencyLayerClient;
import com.example.currencylayer.CurrencyLayerException;
import com.example.currencylayer.CurrencyLayerModels;
import com.example.currencylayer.advanced.AdvancedCurrencyService;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class CurrencyLayerClientTest {
private MockWebServer mockWebServer;
private CurrencyLayerClient client;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
client = new CurrencyLayerClient(
"test-api-key",
mockWebServer.getHostName() + ":" + mockWebServer.getPort(),
false
);
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void testLiveRatesSuccess() throws Exception {
String jsonResponse = """
{
"success": true,
"terms": "https://currencylayer.com/terms",
"privacy": "https://currencylayer.com/privacy",
"timestamp": 1634577963,
"source": "USD",
"quotes": {
"USDEUR": 0.86115,
"USDGBP": 0.73016,
"USDJPY": 113.25
}
}
""";
mockWebServer.enqueue(new MockResponse()
.setBody(jsonResponse)
.addHeader("Content-Type", "application/json"));
CurrencyLayerModels.LiveRatesResponse response = client.getLiveRates();
assertTrue(response.isSuccess());
assertEquals("USD", response.getSource());
assertEquals(3, response.getQuotes().size());
assertEquals(new BigDecimal("0.86115"), response.getQuotes().get("USDEUR"));
// Verify request
RecordedRequest request = mockWebServer.takeRequest();
assertEquals("/live?access_key=test-api-key", request.getPath());
}
@Test
void testLiveRatesWithParameters() throws Exception {
String jsonResponse = """
{
"success": true,
"source": "EUR",
"quotes": {
"EURGBP": 0.84802,
"EURUSD": 1.16115
}
}
""";
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
List<String> currencies = Arrays.asList("GBP", "USD");
CurrencyLayerModels.LiveRatesResponse response = client.getLiveRates("EUR", currencies);
assertEquals("EUR", response.getSource());
assertEquals(2, response.getQuotes().size());
RecordedRequest request = mockWebServer.takeRequest();
assertTrue(request.getPath().contains("source=EUR"));
assertTrue(request.getPath().contains("currencies=GBP,USD"));
}
@Test
void testApiError() {
String jsonResponse = """
{
"success": false,
"error": {
"code": 101,
"type": "invalid_access_key",
"info": "You have not supplied a valid API Access Key."
}
}
""";
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
CurrencyLayerException exception = assertThrows(CurrencyLayerException.class,
() -> client.getLiveRates());
assertTrue(exception.getMessage().contains("invalid_access_key"));
}
@Test
void testHistoricalRates() throws Exception {
String jsonResponse = """
{
"success": true,
"historical": true,
"date": "2023-10-01",
"source": "USD",
"quotes": {
"USDEUR": 0.85115,
"USDGBP": 0.72016
}
}
""";
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
LocalDate date = LocalDate.of(2023, 10, 1);
CurrencyLayerModels.HistoricalRatesResponse response = client.getHistoricalRates(date);
assertTrue(response.isSuccess());
assertEquals(date, response.getDate());
assertEquals(2, response.getQuotes().size());
RecordedRequest request = mockWebServer.takeRequest();
assertTrue(request.getPath().contains("date=2023-10-01"));
}
@Test
void testCurrencyConversion() throws Exception {
String jsonResponse = """
{
"success": true,
"query": {
"from": "USD",
"to": "EUR",
"amount": 100
},
"info": {
"timestamp": 1634577963,
"quote": 0.86115
},
"result": 86.115
}
""";
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
CurrencyLayerModels.CurrencyConversionResponse response =
client.convertCurrency("USD", "EUR", new BigDecimal("100"));
assertTrue(response.isSuccess());
assertEquals(new BigDecimal("86.115"), response.getResult());
RecordedRequest request = mockWebServer.takeRequest();
assertTrue(request.getPath().contains("from=USD"));
assertTrue(request.getPath().contains("to=EUR"));
assertTrue(request.getPath().contains("amount=100"));
}
@Test
void testSupportedCurrencies() throws Exception {
String jsonResponse = """
{
"success": true,
"currencies": {
"USD": "United States Dollar",
"EUR": "Euro",
"GBP": "British Pound Sterling"
}
}
""";
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
CurrencyLayerModels.SupportedCurrenciesResponse response = client.getSupportedCurrencies();
assertTrue(response.isSuccess());
assertEquals(3, response.getCurrencies().size());
assertEquals("United States Dollar", response.getCurrencies().get("USD").getName());
}
}
// Integration test with caching
class AdvancedCurrencyServiceTest {
private MockWebServer mockWebServer;
private CurrencyLayerClient client;
private AdvancedCurrencyService advancedService;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
client = new CurrencyLayerClient(
"test-key",
mockWebServer.getHostName() + ":" + mockWebServer.getPort(),
false
);
advancedService = new AdvancedCurrencyService(client, 1); // 1 minute TTL for testing
}
@Test
void testCachedRates() throws Exception {
String jsonResponse = """
{
"success": true,
"source": "USD",
"quotes": {
"USDEUR": 0.86115,
"USDGBP": 0.73016
}
}
""";
// First request - should call API
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
List<String> currencies = Arrays.asList("EUR", "GBP");
Map<String, BigDecimal> rates1 = advancedService.getCachedLiveRates("USD", currencies);
assertEquals(2, rates1.size());
assertEquals(1, mockWebServer.getRequestCount());
// Second request - should use cache
Map<String, BigDecimal> rates2 = advancedService.getCachedLiveRates("USD", currencies);
assertEquals(rates1, rates2);
assertEquals(1, mockWebServer.getRequestCount()); // Still 1, cache was used
}
@Test
void testBulkConversion() throws Exception {
String jsonResponse = """
{
"success": true,
"source": "USD",
"quotes": {
"USDEUR": 0.86115,
"USDGBP": 0.73016,
"USDJPY": 113.25
}
}
""";
mockWebServer.enqueue(new MockResponse().setBody(jsonResponse));
List<String> toCurrencies = Arrays.asList("EUR", "GBP", "JPY");
Map<String, BigDecimal> results = advancedService.bulkConvert("USD", toCurrencies,
new BigDecimal("100"));
assertEquals(3, results.size());
assertEquals(new BigDecimal("86.115"), results.get("EUR")); // 100 * 0.86115
assertEquals(new BigDecimal("73.016"), results.get("GBP")); // 100 * 0.73016
}
}
Configuration and Deployment
Step 7: Application Configuration
application.yml:
currencylayer:
api-key: ${CURRENCY_LAYER_API_KEY:your_api_key_here}
base-url: api.currencylayer.com
use-https: true
cache-ttl-minutes: 15
server:
port: 8080
spring:
application:
name: currency-service
logging:
level:
com.example.currencylayer: DEBUG
management:
endpoints:
web:
exposure:
include: health,metrics,info
endpoint:
health:
show-details: always
Maven Dependencies (pom.xml):
<?xml version="1.0" encoding="UTF-8"?> <project> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- HTTP Client --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.11.0</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>mockwebserver</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> </dependencies> </project>
Key Features and Benefits
Core Features:
- Real-time Exchange Rates: Get current rates for 168 currencies
- Historical Data: Access historical exchange rates
- Currency Conversion: Convert between any supported currencies
- Time-frame Analysis: Analyze rates over custom time periods
- Supported Currencies: Get detailed information about all available currencies
Advanced Features:
- Intelligent Caching: Reduce API calls with configurable TTL
- Rate Limiting: Built-in respect for API rate limits
- Bulk Operations: Efficient processing of multiple currencies
- Arbitrage Detection: Identify potential arbitrage opportunities
- Currency Strength Analysis: Analyze currency performance and volatility
Integration Benefits:
- Spring Boot Ready: Seamless Spring integration with auto-configuration
- REST API: Full-featured REST controller for easy consumption
- Async Support: Non-blocking operations for better performance
- Comprehensive Testing: Unit and integration test coverage
- Production Ready: Error handling, logging, and monitoring
This comprehensive Currency Layer API integration provides a robust foundation for building financial applications, trading systems, currency converters, and analytical tools with real-time and historical exchange rate data.