Money in Java
If you are developing a financial application with multiple currency, consider using Money representation. Every amount will be associated with Currency so that we can prevent mis-calculation of financial amounts of different currency.
Basically, we want to have arithmetic operations. Currency rounding, currency conversion. When accidentally compute two different currencies, throw exception.
API. There are two popular apis. Moneta ( http://javamoney.github.io/ ) and Joda Money( https://www.joda.org/joda-money/ ).
After doing a comparison. I decided to use Joda Money
Let’s explore how to use Joda Money in our application. We want to achieve a few things:
- Convert Money to JSON and vice-versa
- Use Money in JPA @Entity objects and Spring Repository @Query
- Understand BigMoney vs Money
Let’s get Started
Joda Money in Spring Boot application with JPA / Hibernate.
Sample code project: https://gitlab.com/sntrnyo/joda-money
Download / Clone the git repository, start spring boot application using STS or mvn spring-boot:run in project folder
The sample code expose REST API endpoints. To keep it simplified, I skipped the Service / Business Logic layer where we handle computations, transaction management etc.. in this project. Swagger 2 UI is enabled to see all available APIs. Go to http://localhost:8080/swagger-ui.html#/
Convert Money to Json and vice-versa
When Money object convert to JSON, we want it to be in following format which is more readable.
{
"amount": 12000.99,
"currency": "USD",
"str": "USD 12,000.99"
}
When parsing JSON to Money object, the input JSON should be as following:
{
"amount": 12000.99,
"currency": "USD"
}
Implementation
We need to create a Custom Json Conversion Module. Register it to ObjectMapper.
see sntrnyo.joda.money.json package for creating JodaMoneyModule.
The module can be registered to ObjectMapper in @SpringBootApplication class.
@Autowired
public void configureObjectMapper(final ObjectMapper mapper) {
mapper
.registerModule(new JodaMoneyModule())
.findAndRegisterModules();
}
You can try sample conversion in sample endpoints: @see MoneyEndpoint.java
GET http://localhost:8080/api/money/sample
RESPONE:
{
"amount": 16091.33,
"currency": "USD",
"str": "USD 16,091.33"
}POST http://localhost:8080/api/money/sample
REQUEST:
{
"currency": "USD",
"amount": "12000.99"
}RESPONSE:
{
"amount": 12000.99,
"currency": "USD",
"str": "USD 12,000.99"
}
Use Money in @Entity class
Store in two columns one for currency and one for amount. Create getter setter for Money type. Do not create getter setter for Currency String and BigDecimal amount.
Then in AccountRepository you can do some JPQL for the two fields. However, do note that there is no validation for mixing different currency in SQL level.
Then let’s try creating an account
Retrieve all accounts
So what is the beauty, try depositing USD 100 to the accounts. You should see the currency mis match exception. Also try creating JPY account and see what is the difference. There is no decimal place in JPY and it is already handled by Money.
Understanding Money vs BigMoney
Money honour the scale of the currency. Meaning you cannot create USD with 3 decimal place. Because USD scale is only 2 decimal. It will throw exception.
Money m = Money.of(CurrencyUnit.USD, new BigDecimal("100.123"));java.lang.ArithmeticException: Scale of amount 100.123 is greater than the scale of the currency USD
To overcome this, you can pass in the RoundingMode
Money m = Money.of(CurrencyUnit.USD, new BigDecimal("100.123"),RoundingMode.DOWN);
Any operation ( +-*/ ) with an amount which exceeds scale, there will be exception. So you have to pass in the Rounding Mode
Money m = m.plus(new BigDecimal("123.456"));
java.lang.ArithmeticException: Rounding necessary
BigMoney the scale is not affected. You can create the BigMoney of USD 100.123 without exception. But normally we will use Money because we want to keep the scale of the currency in application.
When to use BigMoney / Money
Before that, let’s see the interest rate computation exercise. In Money
BigDecimal factor = new BigDecimal("1.34678");
Money principle = Money.of(CurrencyUnit.USD, new BigDecimal("1"));Money m = Money.zero(CurrencyUnit.USD);
for(int i =0;i<1000; i++)
{
m = m.plus(principle.multipliedBy(factor, RoundingMode.DOWN));
}
log.debug("M: {}",m.getAmount());// RESULT >> M: 1340.00
In BigMoney
BigDecimal factor = new BigDecimal("1.34678");
BigMoney principle = BigMoney.of(CurrencyUnit.USD, new BigDecimal("1"));
BigMoney m = BigMoney.zero(CurrencyUnit.USD);for(int i =0;i<1000; i++)
{
m = iim.plus(principle.multipliedBy(factor));
}logger.debug("M: {}",m.getAmount());// RESULT >> IIM: 1346.78000
Using Money, there will be a difference of 6.78. Because we need to Round it down.
The amount got rounded to 2 decimals before adding. Using Money data type for rates calculation may have error in calculation, if the business case is to sum all the interest and then perform rounding.
Currency Conversion
Money usd = Money.parse("USD 150");BigDecimal rate = new BigDecimal("111.45");
Money jpy = usd.convertedTo(CurrencyUnit.JPY, rate , RoundingMode.DOWN);logger.debug("jpy: {}", jpy.getAmount());
// RESULT >> jpy: 16717BigMoney busd = BigMoney.parse("USD 150");
BigMoney bjpy = busd.convertedTo(CurrencyUnit.JPY, rate);logger.debug("bjpy: {}", bjpy.getAmount());
// RESULT >> bjpy: 16717.50
We may be discussing more about Currency Converters using FixerAPI and Joda Money in future articles. Stay Tuned. And congratulation, you have reached to the end of the article. Hope to hear your thoughts on this. Please leave some comment.