Recently I added a new backend feature to Finovara — a currency conversion module powered by the public API from the National Bank of Poland (NBP).
The goal was to create a clean and scalable solution that could:
- fetch real exchange rates
- support multiple conversion strategies
- handle external API failures properly
- provide precise financial calculations
This feature became a really solid example of practical backend development with Spring Boot and OpenFeign.
Using Feign to Integrate the NBP API.
To communicate with the NBP API, I used OpenFeign.
The implementation is very clean and keeps the HTTP layer separated from the business logic.
@FeignClient(name = "nbp-api", url = "${nbp.api.url}")
public interface NbpApiClient {
@GetMapping("/exchangerates/tables/A")
List<NbpTableDto> getAllRates(@RequestParam("format") String format);
}
NBP provides public exchange rate tables in JSON format, which makes integration very straightforward.
DTO Mapping
To deserialize the response from the API, I used Java records.
@JsonIgnoreProperties(ignoreUnknown = true)
public record NbpTableDto(
@JsonProperty("table")
String tableType,
@JsonProperty("no")
String tableNumber,
@JsonProperty("effectiveDate")
String publishDate,
@JsonProperty("rates")
List<Rate> rates
) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Rate(
@JsonProperty("currency")
String name,
@JsonProperty("code")
String currencyCode,
@JsonProperty("mid")
BigDecimal averageRate
) {
}
}
I really like using records for DTOs because they keep the code lightweight, immutable, and easy to read.
Currency Conversion Service
The main logic lives inside the NbpService.
The service supports:
PLN → foreign currency
foreign currency → PLN
foreign currency → foreign currency
@Slf4j
@Service
@RequiredArgsConstructor
public class NbpService {
@Value("${nbp.properties.scale}")
private int scale;
private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;
private final NbpApiClient nbpApiClient;
public List<NbpTableDto> getAllRates() {
try{
return nbpApiClient.getAllRates("json");
}catch (FeignException exception){
log.error("Failed to fetch rates from NBP API", exception);
throw new ServiceUnavailableException("Failed to get all rates", exception);
}
}
public BigDecimal convertCurrencies(String fromCurrency, String toCurrency, BigDecimal amount, NbpConversionType conversionType) {
if (fromCurrency.equalsIgnoreCase(toCurrency)) {
return amount.setScale(scale, ROUNDING_MODE);
}
List<NbpTableDto.Rate> exchangeRates = fetchExchangeRates();
return switch (conversionType) {
case FROM_PLN -> convertFromPln(exchangeRates, toCurrency, amount);
case TO_PLN -> convertToPln(exchangeRates, fromCurrency, amount);
case FOREIGN_CURRENCIES -> convertBetweenForeignCurrencies(exchangeRates, fromCurrency, toCurrency, amount);
};
}
private BigDecimal convertFromPln(List<NbpTableDto.Rate> exchangeRates, String toCurrency, BigDecimal amount) {
BigDecimal toRate = findRateByCode(exchangeRates, toCurrency);
return amount.divide(toRate, scale, ROUNDING_MODE);
}
private BigDecimal convertToPln(List<NbpTableDto.Rate> exchangeRates, String fromCurrency, BigDecimal amount) {
BigDecimal fromRate = findRateByCode(exchangeRates, fromCurrency);
return amount.multiply(fromRate).setScale(scale, ROUNDING_MODE);
}
private BigDecimal convertBetweenForeignCurrencies(List<NbpTableDto.Rate> exchangeRates, String fromCurrency, String toCurrency, BigDecimal amount) {
BigDecimal fromRate = findRateByCode(exchangeRates, fromCurrency);
BigDecimal toRate = findRateByCode(exchangeRates, toCurrency);
BigDecimal amountInPln = amount.multiply(fromRate);
return amountInPln.divide(toRate, scale, ROUNDING_MODE);
}
private BigDecimal findRateByCode(List<NbpTableDto.Rate> exchangeRates, String currencyCode) {
return exchangeRates.stream()
.filter(rate -> rate.currencyCode().equalsIgnoreCase(currencyCode))
.findFirst()
.map(NbpTableDto.Rate::averageRate)
.orElseThrow(() -> new InvalidInputException("Unsupported currency: " + currencyCode));
}
private List<NbpTableDto.Rate> fetchExchangeRates() {
List<NbpTableDto> tables = getAllRates();
if (tables == null || tables.isEmpty()) {
throw new InvalidInputException("Exchange rates are currently unavailable.");
}
return tables.getFirst().rates();
}
}
What Was Costly
This wasn’t just a small feature addition.
It required:
- integrating an external API with Feign
- creating DTO mappings for NBP API responses
- implementing multiple conversion strategies
- handling financial precision correctly with BigDecimal
- adding proper exception handling for external API failures
- designing reusable conversion logic
- validating unsupported currencies and invalid requests
Even relatively simple finance-related features become more complex once precision and reliability matter.
Things to Watch Out For
If you’re building something similar, there are a few important things to keep in mind.
Financial Precision
Never use double for currency calculations.
Using BigDecimal together with proper rounding modes is essential for predictable and safe financial calculations.
External API Failures
Public APIs can become unavailable at any moment.
Without proper exception handling and fallback logic, a simple API outage can break part of your application.
That’s why handling exceptions like:
FeignException
and providing meaningful fallback behavior is extremely important.
Validation
Always validate supported currency codes and incoming requests.
Without proper validation, invalid input can easily lead to unexpected runtime issues and broken conversion flows.
Testing
Currency conversion logic should always be covered with tests.
Small rounding inconsistencies or conversion mistakes can quickly become serious problems in finance-related systems.
Thanks for reading!
If you'd like to see Finovara development, check out my GitHub!
M4rc1nek
/
finovara-backend
Backend service for a personal finance management application
Finovara
Backend platform for a personal finance management application, built with Java 25 and Spring Boot 4 as a set of microservices.
About the Project
Finovara is a personal finance platform designed to help users take full control of their money. The backend exposes a secure REST API that powers tracking of income and expenses, budget management, savings goals, shared accounts and financial reporting.
The platform is built as a set of independent services, each responsible for a separate area of the domain, so that individual parts of the system can be developed, deployed and scaled on their own. Security is a central concern: authentication is based on JWT, sessions are tracked per device, and user activity is recorded in an audit log.
Key Features
Accounts and Security
- Authentication and Authorization - stateless JWT security built on Spring Security, with an access and refresh token flow.
- Device and Session Tracking…
Top comments (0)