Modeling money in Java: pitfalls and solutions
Introduction
If you’re a Java programmer, chances are high that your code involves handling some form of money representation. Surprisingly there is no standardised approach for dealing with monetary values. But why does this lack of standardisation pose an issue, and what pitfalls you might encounter? In this article, I’ll explore the common issues associated with modeling money in Java.
Problems with operations on monetary values
If your application operates on a single currency, and is focused on “displaying” and storing money rather than on performing financial calculations, then maybe you are fine. However, you can still encounter issues like:
- floating-point errors
- formatting for presentation
- minor units
- mixing different currencies
- rounding errors
Let’s go through this issues one by one.
Floating point errors
Not all real numbers can be accurately represented in a floating point format using binary representation. The truth is, that even some really simple operations can result in a loss of precision. Here are some examples:
double val = 1.00;
for (int i = 0; i < 10; i++) {
val += 0.10;
}
System.out.println(val); // 2.000000000000001
double first = 0.1;
double second = 0.2;
double result = first + second;
System.out.println(result); // 0.30000000000000004 double a = 1.0000001;
double b = 2.0000002;
double c = 3.0000003; System.out.println((a + b) + c); // 6.0000006
System.out.println(a + (b + c)); // 6.000000600000001
In Java, float and double data types should never be used for precise values, such as currency. For such operations, it’s a better idea to use BigDecimal class:
BigDecimal val = new BigDecimal("1.00");
for (int i = 0; i < 10; i++) {
val = val.add(new BigDecimal("0.10"));
}
System.out.println(val); // 2.00
BigDecimal first = new BigDecimal("0.1");
BigDecimal second = new BigDecimal("0.2");
BigDecimal result = first.add(second);
System.out.println(result); // 0.3 BigDecimal a = new BigDecimal("1.0000001");
BigDecimal b = new BigDecimal("2.0000002");
BigDecimal c = new BigDecimal("3.0000003"); System.out.println((a.add(b)).add(c)); // 6.0000006
System.out.println(a.add(b.add(c))); // 6.0000006
Now as you can see, the results are correct.
You may wonder why I am using BigDecimal(String)
constructor - well, this is another trap. As you can read in the documentation of this constructor:
[…] This is generally the preferred way to convert a float or double into a BigDecimal, as it doesn’t suffer from the unpredictability of the BigDecimal(double) constructor.
System.out.println(new BigDecimal(0.35)); // 0.34999999999999997779553950749686919152736663818359375
System.out.println(new BigDecimal("0.35")); // 0.35
If you want to read more about this problem, you can take a look at the IEEE Standard for Floating-Point Arithmetic (IEEE 754), which defines the way to transform real numbers into a binary format.
You could also use long data type and work on currency minor units, like using 100 (cents) to represent one dollar, and make a rounding to a currency minor unit at the end of calculations.
Formatting for presentation
Same amount can be formatted differently based on a currency and user’s locale. You should take into consideration things like currency symbol location (before/after amount), grouping, decimal separator, user’s locale etc.
BigDecimal amount = new BigDecimal("15000.99");
NumberFormat usFormat = NumberFormat.getCurrencyInstance(Locale.US);
System.out.println(usFormat.format(amount)); // $15,000.99
NumberFormat franceFormat = NumberFormat.getCurrencyInstance(Locale.FRANCE);
System.out.println(franceFormat.format(amount)); // 15 000,99 €
Minor units
Most currencies have two decimals, but some currencies do not have decimals at all (like JPY), and some have three decimals (like BHD).
Mixing different currencies in some operations
Quite self-explanatory, but still it’s possible that one would forget to compare currencies, and focus only on operations on numbers.
Rounding errors
Some currencies may have their own rounding rules, like CHF, where prices have to be rounded to 5 Rappen. For example values between CHF 0,975 and CHF 1,024 will be rounded to CHF 1,00.
Money pattern
At this point you are aware of some pitfalls, and have a starting point to look for a solution. The thing is that not every team member might be aware of them! In a huge and complex applications, if there’s no unified approach to money representation, there is a chance that sooner or later someone will forget to use BigDecimal, or to check currency, or… you get the point. That’s why Money pattern has been introduced.
You can find Money pattern described in “Patterns of Enterprise Application Architecture” book written by Martin Fowler. It advocates for encapsulating currency and amount together as an object. This pattern promotes the use of a custom class to represent monetary values, incorporating information about the currency alongside the amount. This not only ensures precision but also enhances code readability and maintainability. It describes how to create a class representing monetary amount, which will take care of currency verification, rounding, formatting, comparison, and some other utility methods.
public class Money {
private BigDecimal amount; // or long if you want to operate on minor units
private Currency currency;
// methods like add/substract/multiply/allocate
// utility methods for comparison: eq, gt, gte, lt, lte ...
// currency check in every method
// formatting logic
}
Based on the description in the book you can design such class on your own, or… use some existing libraries, like joda-money or JSR 354 API implementation — Moneta. Moneta library (JSR 354) has been even supposed to be added to a JDK! It also has a nice feature of adding new currencies, like bitcoins or… loyalty/reward points. Additionally, you can define your own logic for exchange rates, rounding etc.
If you want to read more about Moneta library you can check its User Guide or this Baeldung article. Bellow it’s the sample code from it’s user guide:
FastMoney m = FastMoney.of(200.20, "USD");
Money m2 = Money.of(200.20, "USD");
Moneta library defines MonetaryAmount interface, which has different implementations like Money (using BigDecimal) and FastMoney (optimised for speed, representing a monetary amount only as a integral number of type long, using a number scale of 100 000).
Conclusion
Effectively modeling money in Java requires careful consideration of data types and precision issues. By adopting the “Money” pattern, as proposed by Martin Fowler, and leveraging specialised libraries, developers can navigate these challenges seamlessly. However, be aware of other potential issues, like:
- choosing a correct data type for storing money in a database
- ensuring that other microservices in your system (frontend and backend ones) and their developers are aware of those issues, and are using the same “standard” when dealing with monetary amounts
Words by Aleksander Kołata, Altimetrik Poland