Fintech

Storing Money in Code: Why Floating Point Breaks Pennies

Try this in a Kotlin Playground: println(0.1 + 0.2 == 0.3). The result is false. Not 0.3—false. Your computer isn't apologizing.

Most developers see this and scroll on, muttering "floating point, classic." They shouldn't. This same representation problem once caused a Patriot missile battery to miss an incoming Scud, killing 28 soldiers.

Why This Happens

The core issue is that 0.1 has no exact representation in binary floating point, just as 1/3 has no exact decimal representation. The actual stored value runs longer than its display—many more digits than anyone would type manually.

Binary float—IEEE 754 double—is the right tool for continuous quantities: physics, animation, interpolation, machine learning feature vectors. Anything with tolerance. Using it for discrete values like money introduces small errors that compound over time.

The reference gives a concrete case: multiplying 0.70 by 1.05 produces a value slightly off from the correct result. One cent off. Run that calculation millions of times and your accounts stop reconciling. IBM estimated floating-point rounding costs the telecom industry $5 million annually.

Kotlin Multiplatform uses IEEE 754 Double on every target, so the platform doesn't change the problem. The fix is never a different runtime—it's a different representation.

Three Honest Ways to Store Money

Binary float (double): fast, but lies slightly.

Integer count of the smallest unit (cents): never lies, but can't divide cleanly.

BigDecimal: always right, never free.

Most money bugs come from someone picking double by accident. But the principle isn't "never use doubles"—it's "choose deliberately." For continuous values with tolerance, double is the correct default.

Using Integer Cents Safely

Count the smallest unit instead of dollars. Store seventy cents instead of seventy dollars of value. That eliminates the 0.1 + 0.2 problem entirely—adding those amounts stored as integer units works exactly.

Stripe uses this approach. A Long covers approximately ±92 quadrillion dollars, enough for most applications.

The trap nobody jumps until production: cents of what? Hardcoding a fixed multiplier is wrong in both directions. Japanese yen have zero decimal places—¥10 is ten units, not ten thousand. Kuwaiti dinar have three—some fractional amounts require hundreds of units. The multiplier comes from the currency's ISO 4217 code, not a magic constant.

And whole cents still can't solve all division problems: certain percentage calculations put the fraction right back, bringing the rounding question with it.

BigDecimal Has Three Traps

BigDecimal is the right answer on the JVM, but it has pitfalls.

The constructor that lies: BigDecimal(0.1) does not produce 0.1—the imprecise binary value gets faithfully preserved. Always use the String constructor: BigDecimal("0.1").

Equality with two answers: BigDecimal("2.0") == BigDecimal("2.00") is false because equals compares scale. Use compareTo() for value comparison. This catches developers off guard when adding values to a HashSet.

Division that refuses to guess: BigDecimal.ONE.divide(BigDecimal("3")) throws ArithmeticException because the result is non-terminating. Always specify a rounding mode or scale for division operations.

The reference notes two rounding modes worth knowing. HALF_EVEN is bank rounding—IEEE 754's default—which reduces systematic bias by rounding 0.5 either direction. HALF_UP is the schoolbook approach, and what tax authorities typically require. One "default" mode rubs someone the wrong way.

Our Analysis: The Real Principle

"Never use floats for money" is a fine bumper sticker and a bad engineering principle. The real rule: choose your representation deliberately based on what you're modeling. If you're storing money, use BigDecimal or integer cents. If you're calculating physics, double is fine.

The Patriot missile failure wasn't caused by a bad algorithm or wrong language. It was the wrong representation for the problem. The system tracked time in tenths of a second, which has no exact binary form. After 100 hours of continuous operation, the accumulated drift reached a third of a second—enough time for an incoming warhead to travel half a kilometer past where the interceptor was aimed.

The same applies to financial calculations in software. Know what you're modeling and choose accordingly.

Practical Example: When Display Rounds Hide Errors

Say you're building a checkout flow. Your code calculates a final price and the display rounds to two decimal places for the customer. Everything looks correct—$32.99, $14.50, whatever.

But underneath, the unrounded value might be slightly off. If your code stores these rounded display values and later sums them for a daily total, small hidden discrepancies compound. The customer sees $32.99, the merchant account settles slightly differently, and after thousands of transactions, your reconciliation reports don't match.

This is why rounding at display time without preserving the full precision underneath creates invisible drift. The fix is calculating with BigDecimal throughout, only rounding at the final display step.

Bottom Line

Floating point errors in financial calculations are real and cumulative. Use BigDecimal with the String constructor for currency, or count integer cents from ISO 4217 currency codes where division isn't required. Know the currency's decimal places, not assumptions. Specify rounding modes explicitly for every division operation. And choose your representation deliberately—your users' money depends on it.

Key takeaways

  • Double stores 0.1 imprecisely; that error accumulates in financial calculations
  • Use BigDecimal with String constructor, or count integer cents from ISO 4217 currency codes
  • BigDecimal has three traps: the double constructor, equals vs compareTo, and division without rounding
  • The principle is "choose deliberately," not "never use doubles"

Frequently asked questions

Why does 0.1 + 0.2 != 0.3 in Kotlin?

Because 0.1 has no exact binary representation in IEEE 754 floating point. The value gets stored with extra digits beyond what displays, causing small rounding errors.

What is the safest way to store money in Kotlin?

Use BigDecimal with the String constructor (BigDecimal("0.1") not BigDecimal(0.1)), or store integer cents using a Long with the currency's ISO 4217 decimal places.

What caused the Patriot missile failure in 1991?

The system tracked time in tenths of a second, which has no exact binary representation. After 100 hours of operation, the accumulated error reached a third of a second—enough time for a Scud missile to travel half a kilometer past where the interceptor was aimed.

Why does BigDecimal("2.0") == BigDecimal("2.00") return false?

BigDecimal's equals() method compares both value and scale. "2.0" and "2.00" have different scales, so they're considered unequal. Use compareTo() for value comparison.

Can Kotlin Multiplatform solve floating point money problems?

No. Kotlin Multiplatform uses IEEE 754 Double on every target, so the representation is the same across platforms. The fix is choosing BigDecimal or integer cents, not changing runtimes.

Related