Code Examples
Java and Spring Boot with the CurrencyCore API
Call /v1/convert and /v1/rates from Spring Boot 3 with RestClient, typed records, connect and read timeouts, and status-preserving errors.
Spring Boot 3.2 or later includes RestClient, added in Spring Framework 6.1. RestTemplate still works but is in maintenance mode. RestClient shares Spring MVC’s converters, so records and BigDecimal map without custom deserializers. It sets connect and read timeouts and preserves upstream status.
Targets Java 17 and Boot 3.2 or later. RestClient ships in spring-web, so one starter is enough.
The official Kotlin/JVM SDK is usable from Java, but this page uses Spring’s RestClient so the client participates in Spring dependency injection, timeouts, and error handling instead of adding a second HTTP stack.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
Bind the key and base URL with a properties record:
@ConfigurationProperties("currencycore")
public record CurrencyCoreProperties(String baseUrl, String apiKey) {}
currencycore:
base-url: https://api.currency-core.com/v1
api-key: ${CURRENCYCORE_API_KEY}
The response record follows the envelope. requestedDate appears only when a different snapshot was served, and asOf lists carried-forward quotes:
public record ConvertResponse(
String from,
BigDecimal amount,
String date,
String requestedDate,
Map<String, String> asOf,
List<Result> results) {
public record Result(String to, BigDecimal result, BigDecimal rate) {}
}
Build one client per application. The JDK HttpClient owns the connect timeout and pool; the factory adds the read timeout:
@Configuration
@EnableConfigurationProperties(CurrencyCoreProperties.class)
class CurrencyCoreConfig {
@Bean
@Qualifier("currencyCoreRestClient")
RestClient currencyCoreRestClient(CurrencyCoreProperties props) {
HttpClient jdk = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(jdk);
factory.setReadTimeout(Duration.ofSeconds(10));
return RestClient.builder()
.baseUrl(props.baseUrl())
.requestFactory(factory)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + props.apiKey())
.build();
}
}
@Component
public class CurrencyCoreClient {
private final RestClient http;
CurrencyCoreClient(@Qualifier("currencyCoreRestClient") RestClient http) {
this.http = http;
}
public ConvertResponse convert(String from, String to, BigDecimal amount, LocalDate date) {
return http.get()
.uri(builder -> builder.path("/convert")
.queryParam("from", from)
.queryParam("to", to)
.queryParam("amount", amount.toPlainString())
.queryParam("date", date)
.build())
.retrieve()
.body(ConvertResponse.class);
}
}
RestClient throws RestClientResponseException for 4xx and 5xx. Keep the body’s code and the HTTP status:
@RestControllerAdvice
class CurrencyCoreExceptionHandler {
private final ObjectMapper json;
CurrencyCoreExceptionHandler(ObjectMapper json) {
this.json = json;
}
@ExceptionHandler(RestClientResponseException.class)
ResponseEntity<ErrorEnvelope> apiError(RestClientResponseException ex) {
ErrorEnvelope body;
try {
body = json.readValue(ex.getResponseBodyAsString(), ErrorEnvelope.class);
} catch (JsonProcessingException e) {
body = new ErrorEnvelope(
new ApiError("upstream_error", "HTTP " + ex.getStatusCode().value()));
}
return ResponseEntity.status(ex.getStatusCode()).body(body);
}
@ExceptionHandler(ResourceAccessException.class)
ResponseEntity<ErrorEnvelope> transportError(ResourceAccessException ex) {
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
.body(new ErrorEnvelope(
new ApiError("upstream_timeout", "CurrencyCore did not respond")));
}
}
record ErrorEnvelope(ApiError error) {}
record ApiError(String code, String message) {}
A conversion returns one entry per target, while /v1/rates/{base} returns a map with the base at 1:
{
"from": "USD",
"amount": 100,
"date": "2026-11-10",
"results": [{ "to": "INR", "result": 8345.0, "rate": 83.45 }]
}
{ "base": "EUR", "date": "2026-11-10", "rates": { "EUR": 1, "USD": 1.0861 } }
The Free plan covers 1,000 requests per month at 60 per minute. Test keys share the same data and quota, always capped at 60 per minute; live keys require a paid plan.
Production notes
RestClient blocks its thread. On Java 21, spring.threads.virtual.enabled=true lets requests wait on virtual threads, which raises concurrency, not upstream speed. Code holding a synchronized lock across I/O can still pin them.
Reuse the client and its JDK pool. A client per request discards pooled connections and TLS sessions and defeats your timeouts.
The API sends no ETag, Last-Modified, or quota headers, so conditional GETs save nothing. Cache rates by base and date, and track monthly usage yourself. Central-bank reference rates are polled hourly, but daily fixings normally change once per business day. Honor Retry-After on 429 when present; otherwise back off.
The SDK list has a Kotlin/JVM client on JitPack, not a Java SDK; this page uses Spring directly.