Java's BigDecimal(double) Constructor Trap: The Precision Tool That Imports Imprecision

2026-05-19

Below is a daily-compound interest routine. The team chose BigDecimal precisely because double arithmetic isn't safe for money. The auditors then reported a $0.83 drift on a single account against their reference spreadsheet. The code compiles, the unit tests on small inputs pass, and the setScale at the end looks responsible. Where is the rot?

import java.math.BigDecimal;
import java.math.RoundingMode;

public class InterestCalculator {
    // Apply daily compounding for `days` days at `dailyRate` on `balance`.
    public static BigDecimal compound(double balance, double dailyRate, int days) {
        BigDecimal b    = new BigDecimal(balance);
        BigDecimal rate = BigDecimal.ONE.add(new BigDecimal(dailyRate));
        for (int i = 0; i < days; i++) {
            b = b.multiply(rate);
        }
        return b.setScale(2, RoundingMode.HALF_UP);
    }

    public static void main(String[] args) {
        // $1,000,000 at 0.0001337 per day for 365 days.
        BigDecimal result = compound(1_000_000.00, 0.0001337, 365);
        System.out.println("Final balance: $" + result);
        // Reference value: $1,051,287.41
        // This prints:     $1,051,288.24
    }
}

The Bug

The defect is on a single line: new BigDecimal(dailyRate). Developers reach for BigDecimal assuming it preserves the number they wrote. But the BigDecimal(double) constructor takes the exact IEEE-754 representation of the double and converts that, not the decimal literal the programmer typed.

The literal 0.0001337 cannot be represented exactly in binary floating point. The nearest double is approximately:

0.00013370000000000000713783100009625104...

So new BigDecimal(0.0001337) produces a 50-digit BigDecimal whose value is that long tail, not 0.0001337. Multiply that by itself 365 times and the tiny excess in the 18th digit compounds into a visible discrepancy in the cents column. The auditors aren't wrong; the code is.

What makes this trap particularly nasty:

The Fix

Two correct approaches:

  1. Never let a double hold a money value in the first place. Take a String at the boundary and feed it to new BigDecimal(String), which parses the decimal literally.
  2. If you're stuck with a double from a legacy API, use BigDecimal.valueOf(double). It routes through Double.toString(), which produces the shortest decimal that round-trips to the same double — i.e., the number the programmer most likely intended.
public static BigDecimal compound(String balance, String dailyRate, int days) {
    BigDecimal b    = new BigDecimal(balance);
    BigDecimal rate = BigDecimal.ONE.add(new BigDecimal(dailyRate));
    // Carry enough precision through the loop; only round at the end.
    java.math.MathContext mc = new java.math.MathContext(20);
    for (int i = 0; i < days; i++) {
        b = b.multiply(rate, mc);
    }
    return b.setScale(2, RoundingMode.HALF_UP);
}

// Caller:
compound("1000000.00", "0.0001337", 365);  // -> 1051287.41

Equivalent rescue when only a double is available:

BigDecimal rate = BigDecimal.ONE.add(BigDecimal.valueOf(dailyRate));

Effective Java item 60 says it directly: "Avoid float and double if exact answers are required." The moment money touches a double, the precision is already gone — wrapping it in BigDecimal afterward just encodes the error with more digits.

Key Takeaway: new BigDecimal(double) preserves the binary float's exact value, not the decimal you wrote — use BigDecimal.valueOf(d) or, better, pass a String so the imprecise double never enters the pipeline.

All newsletters