Tutorial/Java and Spring Boot

Exchange rates in Java and Spring Boot

Build a typed Spring Boot client for live FX rates with RestClient, BigDecimal, environment-based authentication, and explicit freshness fields.

MMexchangerate.dev·Jul 17, 2026·6 min read

Spring RestClient can map exchangerate.dev JSON directly into a Java record. Keep rates as BigDecimal, inject the API key from configuration, and retain source, market_session, and data_updated_at instead of reducing the response to one number.

Key points
Use Spring RestClient for a synchronous typed client and let it reject 4xx and 5xx responses by default.
Map rate values to BigDecimal; do not convert money through binary floating-point arithmetic.
Load the key from EXCHANGERATE_API_KEY, never from a committed properties file.
Store data_updated_at with the rate so downstream jobs can enforce their own freshness rule.

Create a typed RestClient

The response record below keeps the rate map and the fields needed to interpret it. Jackson maps the snake-case JSON names through @JsonProperty, while Spring handles the HTTP request and JSON decoding.

java · ExchangeRateClient.javacopy
package com.example.fx;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

record LatestRates(
    String base,
    String source,
    @JsonProperty("market_session") String marketSession,
    @JsonProperty("data_updated_at") Instant dataUpdatedAt,
    Map<String, BigDecimal> rates
) {}

@Component
public final class ExchangeRateClient {
  private final RestClient client;

  public ExchangeRateClient(
      RestClient.Builder builder,
      @Value("${exchangerate.api-key}") String apiKey
  ) {
    this.client = builder
        .baseUrl("https://api.exchangerate.dev/v1")
        .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
        .build();
  }

  public LatestRates latest(String base) {
    return client.get()
        .uri("/latest/{base}?symbols=EUR,GBP", base)
        .retrieve()
        .body(LatestRates.class);
  }
}

Load the key from the environment

yaml · application.yamlcopy
exchangerate:
  api-key: ${EXCHANGERATE_API_KEY}

Handle freshness and errors explicitly

RestClient.retrieve() throws on 4xx and 5xx responses by default, so a failed upstream call does not become a cached zero. Add bounded timeouts in the shared request factory, and retry only transient failures. A 401 needs a key fix; a sustained 429 needs less traffic or a higher limit.

Keep BigDecimal through the calculation
Parse rates and amounts as BigDecimal, multiply with an explicit rounding policy, and round only for the target currency at the display boundary. The API rate should remain at full precision.
  • Reuse one configured RestClient across requests.
  • Set connection and read timeouts on its request factory.
  • Cache only successful responses.
  • Persist source, marketSession, and dataUpdatedAt with the rate.
MM
exchangerate.dev
Integration guides for developers building with FX data.

Keep reading

GuideHistorical FX rates and time series in one callRead ReferenceReading source and market_sessionRead GuideIndicative vs executable FX ratesRead