Java remains the backbone of enterprise software, powering payment systems, banking platforms, and global e-commerce applications. Integrating an exchange rate API in Java gives these systems access to live currency data without maintaining rate tables manually. This tutorial walks you through a complete Spring Boot integration, from HTTP clients to scheduled cache refresh.

Why Use an Exchange Rate API in Java Applications?

Enterprise Java applications often deal with multi-currency transactions. Manually managing exchange rates is error-prone and creates a maintenance burden that grows with every currency you support. An exchange rate API in Java solves this by providing real-time data through a simple REST call. The Exchange Rate API supports 160+ currencies, uses straightforward bearer token authentication, and returns clean JSON that maps directly to Java DTOs.

Prerequisites

Project Setup

Add the necessary dependencies to your pom.xml:


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>
    </dependencies>
    

Configure the API details in application.yml:


    exchange-rate:
      api-key: ${EXCHANGE_RATE_API_KEY}
      base-url: https://api.allratestoday.com/v1
      cache-duration: 60m
    

DTO Classes

Define Java records to model the API responses. Records are ideal here because the data is immutable once received.


    package com.example.currency.dto;
    
    import java.util.Map;
    
    public record LatestRatesResponse(
        String base,
        String date,
        Map<String, Double> rates
    ) {}
    
    public record ConversionResponse(
        String from,
        String to,
        double amount,
        double result,
        double rate
    ) {}
    
    public record TimeSeriesResponse(
        String base,
        String startDate,
        String endDate,
        Map<String, Map<String, Double>> rates
    ) {}
    

Configuration Class

Create a configuration properties class and an HTTP client bean:


    package com.example.currency.config;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = "exchange-rate")
    public record ExchangeRateProperties(
        String apiKey,
        String baseUrl,
        String cacheDuration
    ) {}
    

    package com.example.currency.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.reactive.function.client.WebClient;
    
    @Configuration
    public class WebClientConfig {
    
        @Bean
        public WebClient exchangeRateWebClient(ExchangeRateProperties props) {
            return WebClient.builder()
                .baseUrl(props.baseUrl())
                .defaultHeader("Authorization", "Bearer " + props.apiKey())
                .defaultHeader("Accept", "application/json")
                .build();
        }
    }
    

Service Layer with RestTemplate

For applications that prefer the traditional blocking approach, here is a service using RestTemplate:


    package com.example.currency.service;
    
    import com.example.currency.dto.LatestRatesResponse;
    import com.example.currency.dto.ConversionResponse;
    import com.example.currency.config.ExchangeRateProperties;
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.http.*;
    import org.springframework.stereotype.Service;
    import org.springframework.web.client.RestTemplate;
    import org.springframework.web.util.UriComponentsBuilder;
    
    @Service
    public class ExchangeRateService {
    
        private final RestTemplate restTemplate;
        private final ExchangeRateProperties props;
    
        public ExchangeRateService(
                RestTemplateBuilder builder,
                ExchangeRateProperties props) {
            this.props = props;
            this.restTemplate = builder
                .defaultHeader("Authorization", "Bearer " + props.apiKey())
                .build();
        }
    
        @Cacheable(value = "latestRates", key = "#base")
        public LatestRatesResponse getLatestRates(String base) {
            String url = UriComponentsBuilder
                .fromHttpUrl(props.baseUrl() + "/latest")
                .queryParam("base", base)
                .toUriString();
    
            ResponseEntity<LatestRatesResponse> response = restTemplate.getForEntity(
                url, LatestRatesResponse.class
            );
    
            if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
                throw new ExchangeRateException("Failed to fetch latest rates");
            }
    
            return response.getBody();
        }
    
        public ConversionResponse convert(String from, String to, double amount) {
            String url = UriComponentsBuilder
                .fromHttpUrl(props.baseUrl() + "/convert")
                .queryParam("from", from)
                .queryParam("to", to)
                .queryParam("amount", amount)
                .toUriString();
    
            ResponseEntity<ConversionResponse> response = restTemplate.getForEntity(
                url, ConversionResponse.class
            );
    
            if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
                throw new ExchangeRateException("Conversion failed");
            }
    
            return response.getBody();
        }
    
        @Cacheable(value = "historicalRates", key = "#date + '_' + #base")
        public LatestRatesResponse getHistoricalRates(String date, String base) {
            String url = UriComponentsBuilder
                .fromHttpUrl(props.baseUrl() + "/historical")
                .queryParam("date", date)
                .queryParam("base", base)
                .toUriString();
    
            return restTemplate.getForObject(url, LatestRatesResponse.class);
        }
    }
    

Reactive Service with WebClient

For non-blocking applications, use Spring WebFlux's WebClient. This is especially useful in high-throughput systems where you cannot afford to block threads waiting for an exchange rate API in Java.


    package com.example.currency.service;
    
    import com.example.currency.dto.*;
    import org.springframework.stereotype.Service;
    import org.springframework.web.reactive.function.client.WebClient;
    import reactor.core.publisher.Mono;
    
    @Service
    public class ReactiveExchangeRateService {
    
        private final WebClient client;
    
        public ReactiveExchangeRateService(WebClient exchangeRateWebClient) {
            this.client = exchangeRateWebClient;
        }
    
        public Mono<LatestRatesResponse> getLatestRates(String base) {
            return client.get()
                .uri(uriBuilder -> uriBuilder
                    .path("/latest")
                    .queryParam("base", base)
                    .build())
                .retrieve()
                .onStatus(status -> !status.is2xxSuccessful(),
                    resp -> Mono.error(new ExchangeRateException(
                        "API returned " + resp.statusCode())))
                .bodyToMono(LatestRatesResponse.class);
        }
    
        public Mono<ConversionResponse> convert(String from, String to, double amount) {
            return client.get()
                .uri(uriBuilder -> uriBuilder
                    .path("/convert")
                    .queryParam("from", from)
                    .queryParam("to", to)
                    .queryParam("amount", amount)
                    .build())
                .retrieve()
                .bodyToMono(ConversionResponse.class);
        }
    
        public Mono<TimeSeriesResponse> getTimeSeries(
                String start, String end, String base) {
            return client.get()
                .uri(uriBuilder -> uriBuilder
                    .path("/timeseries")
                    .queryParam("start", start)
                    .queryParam("end", end)
                    .queryParam("base", base)
                    .build())
                .retrieve()
                .bodyToMono(TimeSeriesResponse.class);
        }
    }
    

REST Controller

Expose the exchange rate data through your own API endpoints:


    package com.example.currency.controller;
    
    import com.example.currency.dto.*;
    import com.example.currency.service.ExchangeRateService;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Map;
    
    @RestController
    @RequestMapping("/api/currency")
    public class CurrencyController {
    
        private final ExchangeRateService service;
    
        public CurrencyController(ExchangeRateService service) {
            this.service = service;
        }
    
        @GetMapping("/rates")
        public ResponseEntity<Map<String, Double>> getRates(
                @RequestParam(defaultValue = "USD") String base) {
            LatestRatesResponse response = service.getLatestRates(base);
            return ResponseEntity.ok(response.rates());
        }
    
        @GetMapping("/convert")
        public ResponseEntity<ConversionResponse> convert(
                @RequestParam String from,
                @RequestParam String to,
                @RequestParam double amount) {
            return ResponseEntity.ok(service.convert(from, to, amount));
        }
    
        @GetMapping("/historical")
        public ResponseEntity<Map<String, Double>> historical(
                @RequestParam String date,
                @RequestParam(defaultValue = "USD") String base) {
            LatestRatesResponse response = service.getHistoricalRates(date, base);
            return ResponseEntity.ok(response.rates());
        }
    }
    

Caching with Caffeine

Enable caching to reduce redundant API calls. The free tier gives you 1,500 requests per month, and caching ensures you use them wisely.


    package com.example.currency.config;
    
    import com.github.benmanes.caffeine.cache.Caffeine;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cache.caffeine.CaffeineCacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.concurrent.TimeUnit;
    
    @Configuration
    @EnableCaching
    public class CacheConfig {
    
        @Bean
        public CacheManager cacheManager() {
            CaffeineCacheManager manager = new CaffeineCacheManager(
                "latestRates", "historicalRates"
            );
            manager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.HOURS)
                .maximumSize(100));
            return manager;
        }
    }
    

Scheduled Rate Refresh

For dashboards or pricing engines that display rates continuously, pre-warm the cache on a schedule:


    package com.example.currency.scheduler;
    
    import com.example.currency.service.ExchangeRateService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cache.CacheManager;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    
    @Component
    @EnableScheduling
    public class RateRefreshScheduler {
    
        private static final Logger log = LoggerFactory.getLogger(RateRefreshScheduler.class);
        private static final List<String> BASE_CURRENCIES = List.of("USD", "EUR", "GBP");
    
        private final ExchangeRateService service;
        private final CacheManager cacheManager;
    
        public RateRefreshScheduler(ExchangeRateService service, CacheManager cacheManager) {
            this.service = service;
            this.cacheManager = cacheManager;
        }
    
        @Scheduled(fixedRate = 3600000) // Every hour
        public void refreshRates() {
            log.info("Refreshing exchange rate cache...");
    
            var cache = cacheManager.getCache("latestRates");
            if (cache != null) {
                cache.clear();
            }
    
            for (String base : BASE_CURRENCIES) {
                try {
                    service.getLatestRates(base);
                    log.info("Refreshed rates for {}", base);
                } catch (Exception e) {
                    log.error("Failed to refresh rates for {}", base, e);
                }
            }
        }
    }
    

Exception Handling

Add a global exception handler for clean error responses:


    package com.example.currency.exception;
    
    import com.example.currency.service.ExchangeRateException;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.Map;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(ExchangeRateException.class)
        public ResponseEntity<Map<String, String>> handleExchangeRateException(
                ExchangeRateException ex) {
            return ResponseEntity
                .status(HttpStatus.BAD_GATEWAY)
                .body(Map.of(
                    "error", "exchange_rate_error",
                    "message", ex.getMessage()
                ));
        }
    }
    

    package com.example.currency.service;
    
    public class ExchangeRateException extends RuntimeException {
        public ExchangeRateException(String message) {
            super(message);
        }
    }
    

Testing

Spring Boot makes it simple to test the service layer using MockRestServiceServer or WireMock:


    @SpringBootTest
    class ExchangeRateServiceTest {
    
        @Autowired
        private ExchangeRateService service;
    
        @Test
        void shouldFetchLatestRates() {
            LatestRatesResponse response = service.getLatestRates("USD");
    
            assertNotNull(response);
            assertFalse(response.rates().isEmpty());
            assertTrue(response.rates().containsKey("EUR"));
        }
    
        @Test
        void shouldConvertCurrency() {
            ConversionResponse result = service.convert("USD", "EUR", 100);
    
            assertNotNull(result);
            assertTrue(result.result() > 0);
            assertEquals("USD", result.from());
            assertEquals("EUR", result.to());
        }
    }
    

Conclusion

Integrating an exchange rate API in Java using Spring Boot follows the clean, layered architecture that enterprise teams expect. You get DTOs for type safety, a service layer for business logic, caching to minimize API calls, and scheduled refresh to keep data fresh. Whether you use the blocking RestTemplate or the reactive WebClient, the exchange rate API fits naturally into the Spring ecosystem.

The Exchange Rate API provides 160+ currencies, a free tier of 1,500 requests per month, and the reliability that production Java applications demand.

Ready to add live exchange rates to your Java application? Sign up for a free API key at exchange-rateapi.com and follow the API documentation to start integrating today.

Start Using the Exchange Rate API Today

Free tier with 1,500 requests/month. 160+ currencies updated every 60 seconds. No credit card required.

Get Your Free API Key →

Related Articles