google-site-verification: google61fe8ba583a51912.html
Java Currency Layer API Integration: Comprehensive Guide

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:

  1. Real-time Exchange Rates: Get current rates for 168 currencies
  2. Historical Data: Access historical exchange rates
  3. Currency Conversion: Convert between any supported currencies
  4. Time-frame Analysis: Analyze rates over custom time periods
  5. Supported Currencies: Get detailed information about all available currencies

Advanced Features:

  1. Intelligent Caching: Reduce API calls with configurable TTL
  2. Rate Limiting: Built-in respect for API rate limits
  3. Bulk Operations: Efficient processing of multiple currencies
  4. Arbitrage Detection: Identify potential arbitrage opportunities
  5. Currency Strength Analysis: Analyze currency performance and volatility

Integration Benefits:

  1. Spring Boot Ready: Seamless Spring integration with auto-configuration
  2. REST API: Full-featured REST controller for easy consumption
  3. Async Support: Non-blocking operations for better performance
  4. Comprehensive Testing: Unit and integration test coverage
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper