Damian Garstecki

back end developer

How safe BigDecimal really is?

· 4 min read

BigDecimal is the go-to type for holding decimal numbers. It's used almost every time high precision is required, and we can't afford rounding issues. But not everything with it is so great. BigDecimal exposes a trap for developers. Let's take a look at what that is and how to secure our code against it.

Why do we even need BigDecimal?

Doubles are not the greatest when it comes to computation with precision. Take a look at this example - we want to add three 0.1, the outcome should be 0.3, but it's not...

In some cases that might not be an issue. For instance, if we want to know the average temperature for the past last ten days - we can live with that slight offset. But in some other cases, we can't have any discrepancies - for instance, if we are dealing with money. Luckily Java comes with BigDecimal type to help with that issue. Adding three BigDecimal(0.1) indeed give us the correct result BigDecimal(0.3). Perfect! This is what BigDecimal is for.

Does it mean that case is closed? BigDecimal is a safe type to perform arithmetic calculations? Yes and no. Indeed BigDecimal gives us a precise result, but there is a pitfall, that we need to talk about. Let's review a couple of different ways of initiating the BigDecimal.

Every single way we initialize the BigDecimal gives us a result that we expected, except the first one. Passing double to the constructor seems like a default way of creating BigDecimal - and probably is the first approach someone would try if they have to make a BigDecimal. But the outcome is completely off. To know why that is, let's read the comment on that constructor.

The very first sentence illustrates our situation perfectly:

The results of this constructor can be somewhat unpredictable.

Oh yes, we can tell. The rest of the doc explains why it works this way, you can read the whole thing here

The default constructor is unpredictable. Unpredictability is something we would like to avoid at all costs. So let's avoid using that constructor altogether. BigDecimal.valueOf(0.1) doesn't have the same problem since JVM will first cast double to String before passing it into the constructor. The same approach is used by Kotlin with 0.1.toBigDecimal() helper. Those two methods are safe to use.

But having an unpredictable constructor in a mix has further-reaching consequences for our code. Let's take a look at an example where we might want to use BigDecimal. The Value Object that will store Money. The most straightforward way of doing so in Kotlin might look like this:

Please notice that we are accepting BigDecimal as a parameter. Hence, we are accepting object that has been already built and we don't have any control over how. And as we know there is a bad and good way of creating BigDecimal. To prevent our Money object users from getting themself into trouble we need to construct our code in a way that it will be impossible to use badly-constructed BigDecimal.

In the new updates class, we expect the user to provide the amount as double - never as BigDecimal. Double is then transformed to BigDecimal in a controlled way and stored inside a value object. We store it as BigDecimal since we still prefer to make a computation on BigDecimal, not the double.

Also, we are removing data from the class. Leaving data will add .clone() method that will allow clients to overcome the constructor and pass BigDecimal directly inside the model.

TL;DR

BigDecimal is safe to perform arithmetic operations. But one of the constructors is working unpredictably. Therefore we should never accept BigDecimal as a param into the Value Object. Accept primitive values and take care of constructing BigDecimal by yourself.

If you are using Sonar Cube one of the default rules will check for any calls to the default constructor and highlight them as errors.