Damian Garstecki
4 min read

How safe is BigDecimal, really?

BigDecimal is the go‑to type for holding decimal numbers. It’s used whenever high precision is required and we can’t afford rounding issues. But not everything about it is great. BigDecimal exposes a trap for developers. Let’s look at what that is and how to secure our code against it.

Why do we even need BigDecimal?

Double is not great for precise decimal arithmetic. Take a look at this example: we want to add three 0.1s; the outcome should be 0.3, but it’s not…

fun main() {
//sampleStart
    val sum = 0.1 + 0.1 + 0.1
    println(sum) // 0.30000000000000004
//sampleEnd
}

In some cases, that might not be an issue. For instance, if we want to know the average temperature for the last ten days—we can live with that slight offset. But in other cases, we can’t have any discrepancies—for instance, when dealing with money. Luckily, Java comes with the BigDecimal type to help with that issue. Using BigDecimal (after converting 0.1 to BigDecimal first) indeed gives us the correct result: 0.3. Perfect! This is what BigDecimal is for.

fun main() {
//sampleStart
    val sum = 0.1.toBigDecimal() + 0.1.toBigDecimal() + 0.1.toBigDecimal()
    println(sum) // 0.3
//sampleEnd
}

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

import java.math.BigDecimal
import java.math.MathContext

fun main() {
//sampleStart
    println(BigDecimal(0.1)) // 0.1000000000000000055511151231257827021181583404541015625
    println(BigDecimal.valueOf(0.1)) // 0.1
    println(BigDecimal("0.1")) // 0.1
    println(0.1.toBigDecimal()) // 0.1
    println(BigDecimal(0.1, MathContext.DECIMAL64)) // 0.1000000000000000
//sampleEnd
}

Every way we initialize the BigDecimal gives us the result we expect—except the first one. Passing a double to the constructor seems like the default way of creating a BigDecimal—and it’s probably the first approach someone tries. 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 double‑argument constructor is unpredictable. Unpredictability is something we’d 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 the JVM converts the double to a String before passing it into the constructor. The same approach is used by Kotlin with the 0.1.toBigDecimal() helper. Those two methods are safe to use.

But having an unpredictable constructor in the mix has further‑reaching consequences for our code. Let’s look at an example where we might want to use BigDecimal: a Value Object to store money. The most straightforward way in Kotlin might look like this:

import java.math.BigDecimal
data class Money(val amount: BigDecimal)

fun main() {
    val bad = Money(BigDecimal(0.1))
    println(bad)
    val good = Money(0.1.toBigDecimal())
    println(good)
}

Notice that we are accepting BigDecimal as a parameter. Hence, we are accepting an object that has already been built—and we don’t control how. And as we know, there is a good and a bad way of creating BigDecimal. To prevent consumers of Money from getting themselves into trouble, we should design our code so it’s impossible to use a badly constructed BigDecimal.

import java.math.BigDecimal

class Money private constructor(val amount: BigDecimal) {
    constructor(amount: Double) : this(amount.toBigDecimal())
    override fun toString() = "Money(amount=${amount})"
}

fun main() {
    val best = Money(0.1)
    println(best)
}

In the updated class, we expect the caller to provide the amount as a Double—never as a BigDecimal. Double is then transformed to BigDecimal in a controlled way and stored inside the Value Object. We store it as BigDecimal since we still prefer to do computations on BigDecimal, not on a double.

Also, we are removing data from the class. Leaving data would add a generated copy() method that allows callers to construct new instances with arbitrary values, bypassing the intended construction path.

TL;DR

BigDecimal is safe for arithmetic operations, but the BigDecimal(double) constructor behaves unpredictably. Therefore, don’t accept BigDecimal directly as a parameter to a Value Object. Accept primitives or strings and construct the BigDecimal yourself in a controlled way.

If you are using SonarQube, one of the default rules will check for any calls to the double‑argument constructor and highlight them as issues.

javakotlinmaintenance