Money looks simple until a single cent sneaks away and your numbers do not match the bank. Java gives you many ways to represent it, some friendly, some dangerous.
Let us talk about precision, rounding, and reality so your totals land exactly where your accountant expects.
Precision beats convenience
Floating point feels easy. It is also how people end up with a receipt that says 19.999999. Binary floats cannot exactly represent values like 0.1 or 0.01, and money lives on those values all day. The fix is boring and proven. Use BigDecimal with the string constructor and set your scale on purpose. That keeps every cent exact across math, storage, and JSON.
Store amounts as minor units when you can cents for USD, pennies for GBP and only turn them into pretty strings at the edge. If you do store decimals, lock scale and rounding early and enforce it with tests. This avoids the haunted bug where a later operation changes the number of fraction digits and breaks comparisons.
// Bad: binary float math
double subtotal = 0.1 + 0.2; // 0.30000000000000004
System.out.println(subtotal);
// Good: exact math with BigDecimal
BigDecimal a = new BigDecimal("0.10");
BigDecimal b = new BigDecimal("0.20");
BigDecimal total = a.add(b); // 0.30 exactly
// Always set scale and rounding for money
BigDecimal money = total.setScale(2, RoundingMode.HALF_EVEN);Rounding is a product choice
Rounding is not a math problem. It is a business rule. Accounting tools expect bankers rounding which in Java is RoundingMode.HALF_EVEN. Point of sale might round cash to five cents. Some tax codes require rounding at the line, others at the invoice. The rule you pick changes totals, so write it down and test for it.
Also, not every currency uses two decimals. JPY has zero fraction digits. Some currencies change rules when inflation hits. Use java.util.Currency to get default fraction digits, then lock your scale to that or to a product rule you own. Mix that with clear naming so no one adds a three decimal crypto amount to a two decimal receipt.
// Currency aware rounding
Currency usd = Currency.getInstance("USD");
int scale = usd.getDefaultFractionDigits(); // usually 2
BigDecimal price = new BigDecimal("19.995");
BigDecimal rounded = price.setScale(scale, RoundingMode.HALF_EVEN); // 20.00
// Tax per line vs tax on subtotal
BigDecimal rate = new BigDecimal("0.0875"); // 8.75 percent
BigDecimal line = new BigDecimal("12.49");
BigDecimal taxPerLine = line.multiply(rate)
.setScale(scale, RoundingMode.HALF_EVEN); // round here
BigDecimal subtotal = line.multiply(new BigDecimal("3"));
BigDecimal taxOnSubtotal = subtotal.multiply(rate)
.setScale(scale, RoundingMode.HALF_EVEN); // or round here
// These can differ by a cent. Pick one rule and stick to it.Java Money moves the pieces into place
There is fresh work in this area with JSR 354 also called Java Money and Currency. It brings types for amounts and currencies, conversion providers, and sane operators for rounding and formatting. The reference implementation is called Moneta, and it is easy to drop into a Java 8 project. If your stack is not ready for it, wrap BigDecimal behind your own Money type with the same ideas names and rules stay readable and safe.
The goal is not clever code. The goal is repeatable numbers across services and days. That means a single place for rounding modes, currency scale, and conversion rules. If you are doing exchange rates, lock a provider and a timestamp so totals match a given day. Put that into tests with a few real receipts and a synthetic case full of fives and halves to catch midpoint ties.
// Example with JSR 354 Moneta
import javax.money.*;
import org.javamoney.moneta.Money;
import org.javamoney.moneta.function.MonetaryOperators;
CurrencyUnit usd = Monetary.getCurrency("USD");
MonetaryAmount unit = Money.of(19.995, usd);
// Bankers rounding to currency scale
MonetaryAmount rounded = unit.with(MonetaryOperators.rounding());
// Add and multiply are exact on amount types
MonetaryAmount subtotal = rounded.multiply(3);
MonetaryAmount tax = subtotal.multiply(0.0875).with(MonetaryOperators.rounding());
MonetaryAmount total = subtotal.add(tax);
// Optional conversion
// ExchangeRateProvider provider = MonetaryConversions.getExchangeRateProvider("ECB");
// MonetaryAmount eurTotal = total.with(MonetaryConversions.getConversion("EUR"));Treat money as a first class type and the cents will stay where you put them.