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.
RestClient for a synchronous typed client and let it reject 4xx and 5xx responses by default.BigDecimal; do not convert money through binary floating-point arithmetic.EXCHANGERATE_API_KEY, never from a committed properties file.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.
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
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.
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.
