DEV Community

Cover image for Building a Currency Converter in Spring Boot with Feign and NBP API - Finovara
Marcin Parśniak
Marcin Parśniak

Posted on

Building a Currency Converter in Spring Boot with Feign and NBP API - Finovara

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);
}

Enter fullscreen mode Exit fullscreen mode

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
    ) {
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

GitHub logo M4rc1nek / finovara-backend

Backend service for a personal finance management application

Finovara

Finovara is a financial management platform designed to help users effectively track analyze, and optimize their income, expenses, and savings The application provides a secure, bank-like experience focused on transparency, financial awareness, and long-term money planning.

🎯 Purpose of the Application

Finovara aims to support users in making better financial decisions by offering clear insights into their financial activity and helping them maintain control over their budgets and savings.

The platform focuses on:

  • organizing income and expenses in a structured way
  • visualizing financial data through charts and statistics
  • supporting saving goals and spending limits
  • providing a virtual wallet concept for daily financial management

🚀 Key Features

  • Secure user authentication and authorization
  • Income and expense tracking
  • Categorization of financial operations
  • Interactive charts and financial statistics
  • Reports summarizing spending and income trends
  • Virtual wallet management
  • Savings goals (e.g. piggy banks)
  • Spending limits and budget control
  • Scalable architecture prepared for future financial…

Top comments (0)