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
Backend REST API for a personal finance management application built with Java 25 and Spring Boot 4.
📖 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, and financial reporting — all wrapped in a bank-grade security model based on JWT authentication.
The application is designed with scalability in mind and is fully containerized via Docker, with separate production and test database environments managed through Docker Compose.
🎯 Key Features
- 🔐 Authentication & Authorization — JWT-based stateless security with Spring Security; access and refresh token flow with device/user-agent detection
- 💸 Income & Expense Tracking — full CRUD for financial operations with category tagging
- 📊 Statistics & Reports — aggregated financial summaries, spending trends, and exportable PDF reports
- 🏦…
Top comments (0)