Why Floats Are a Financial Fiasco: Modern Treasury's Precision Play with Integers
Share this article
In finance, every cent counts—literally. Yet, developers often default to IEEE-754 floating-point numbers for storing currency, seduced by their simplicity and language ubiquity. This choice, however, is a ticking time bomb for systems handling payments, balances, and transactions. As Modern Treasury's deep dive reveals, floats introduce insidious errors that can cascade into million-dollar discrepancies. Here’s why precision matters, and how integers offer a bulletproof alternative.
The Hidden Perils of Floating-Point Arithmetic
Floats represent numbers in base-2 binary, splitting values into a significand and exponent to cover vast ranges efficiently. But this efficiency comes at a cost: limited significant digits and base-2 approximations that butcher decimal values. For example, $2.78 stored as a float becomes 2.7799999713897705078125—a microscopic error that snowballs in financial operations.
Decimal vs. floating-point representation highlights how base-2 struggles with everyday cents.
Consider a bank verifying microdeposits:
# Ruby example: Float comparison fails for $0.18
amount_entered = 0.18
amount_stored = 0.55 - 0.37 # Evaluates to 0.17999999999999999
puts amount_entered == amount_stored # False—verification breaks
Workarounds like tolerance thresholds or explicit rounding add complexity, but they’re band-aids on a deeper wound.
Rounding exacerbates the chaos. IEEE-754 defines five modes (e.g., banker’s rounding, floor), but implementations vary wildly:
- Ruby rounds half away from zero, while Python uses half-even.
- JavaScript defaults to half toward positive infinity.
Rounding discrepancies across languages and sign types create financial blind spots.
These inconsistencies are not academic. They’ve skewed elections, cratered stocks, and even altered missile trajectories. As Modern Treasury notes, relying on floats means fighting subtle traps like:
- Two zeros: Positive and negative zero have different binary representations, risking equality checks.
- Error cascades: Adding values in different orders can shift totals by cents, as in:
# Banker’s rounding in Ruby
values = [1.5, 2.5, 3.5]
puts values.sum.round # 8 (if summed first: 1.5+2.5=4.0, +3.5=7.5→8)
puts values.map(&:round).sum # 7 (1.5→2, 2.5→2, 3.5→4)
Integers: The Precision Powerhouse
Modern Treasury sidesteps this minefield by storing all monetary values as 64-bit integers, representing cents (e.g., $12.34 becomes 1234). This approach guarantees exactness, leveraging:
- Hardware optimization: Int64 operations execute in a single CPU cycle, enabling billion-transaction scalability.
- Storage efficiency: Fixed-width bigint fields in PostgreSQL (Ruby’s Integer backend) ensure compact, fast database reads.
- Currency agnosticism: Paired with ISO 4217 codes, integers handle subdivisions like yen or euros without rounding.
The range is staggering: ±92 quadrillion dollars—far exceeding global GDP. For edge cases beyond int64 (e.g., cryptocurrency), BigNumber or BigDecimal offer alternatives, but integers remain ideal for mainstream finance.
When Alternatives Fall Short
Other methods introduce trade-offs:
- BigDecimal: Exact but slower, with variable storage overhead.
- Strings: Simple storage but crippled for arithmetic.
- Separate units: Storing dollars and cents apart complicates math and doubles memory use.
For developers, the lesson is clear: floats belong in graphics or simulations—not finance. Modern Treasury’s integer-first ethos eliminates approximation risks, ensuring payments reconcile perfectly. As digital transactions explode, precise money handling isn’t just best practice—it’s non-negotiable.