Contents

Before you write a single line of financial code in Java, run this program. It is the best 30-second education in numerical computing you will ever get:

public class FloatingPointTrap { public static void main(String[] args) { double a = 0.1; double b = 0.2; System.out.println(a + b); // 0.30000000000000004 System.out.println(a + b == 0.3); // false double price = 1.10; double qty = 3; System.out.println(price * qty); // 3.3000000000000003 } }

That extra 0.00000000000000004 is not a bug in Java — it's a direct consequence of how IEEE 754 binary floating-point works. A double stores numbers as sign × mantissa × 2exponent. The fraction 0.1 in base 2 is an infinitely repeating pattern (0.0001100110011…), just like 1/3 is in base 10. The hardware truncates it, and every subsequent operation compounds the tiny error.

A double has about 15–17 decimal digits of precision. For scientific work where an error in the 16th digit is irrelevant, it's perfect. For a bank statement where a single cent must be correct across billions of transactions, it is catastrophic.
The long overflow trap

Integers have a different failure mode: silent overflow. A long holds values up to about 9.2 × 1018. Factorials, RSA keys, and combinatorial counts blow past that instantly:

long factorial = 1; for (int i = 1; i <= 25; i++) { factorial *= i; } System.out.println(factorial); // 7034535277573963776 (WRONG — overflow at 21!) // BigInteger handles it effortlessly: java.math.BigInteger big = java.math.BigInteger.ONE; for (int i = 1; i <= 25; i++) { big = big.multiply(java.math.BigInteger.valueOf(i)); } System.out.println(big); // 15511210043330985984000000
When you must reach for BigDecimal and BigInteger

The fix for both traps lives in the same package — java.math:

import java.math.BigDecimal; BigDecimal a = new BigDecimal("0.1"); BigDecimal b = new BigDecimal("0.2"); System.out.println(a.add(b)); // 0.3 (exact) System.out.println(a.add(b).equals(new BigDecimal("0.3"))); // true

The single most important rule when creating a BigDecimal is: always use the String constructor, never the double constructor.

The new BigDecimal(double) trap. The double constructor converts whatever inexact binary approximation the JVM stored — and faithfully preserves that error into your BigDecimal. Watch: System.out.println(new BigDecimal(0.1)); // 0.1000000000000000055511151231257827021181583404541015625 System.out.println(new BigDecimal("0.1")); // 0.1 (exactly what you wanted) The String constructor parses the decimal digits directly and never touches floating-point hardware, so what you type is what you get.
The creation cheat-sheet
import java.math.BigDecimal; // 1. String constructor — always safe, always preferred BigDecimal a = new BigDecimal("1.23"); BigDecimal b = new BigDecimal("9999999999999999999.9999"); // arbitrary size, no overflow // 2. valueOf(double) — also safe; internally uses Double.toString() BigDecimal c = BigDecimal.valueOf(0.1); // 0.1 (good) BigDecimal d = BigDecimal.valueOf(1.23); // 1.23 // 3. valueOf(long) — safe for integer values BigDecimal e = BigDecimal.valueOf(1000L); // 1000 // 4. valueOf(long unscaledValue, int scale) — store raw digits + decimal position BigDecimal f = BigDecimal.valueOf(12345L, 2); // 123.45 // 5. Constants — use these, don't create new objects BigDecimal zero = BigDecimal.ZERO; // 0 BigDecimal one = BigDecimal.ONE; // 1 BigDecimal ten = BigDecimal.TEN; // 10 // 6. From int or long BigDecimal g = new BigDecimal(42); // 42 (int constructor is safe) BigDecimal h = new BigDecimal(1_000_000_000L); // 1000000000 // 7. NEVER do this in production code BigDecimal bad = new BigDecimal(0.1); System.out.println(bad); // 0.1000000000000000055511151231257827021181583404541015625 BigDecimal.valueOf(double) is a convenient shortcut that calls Double.toString(d) under the hood. It's safe for literal doubles, but if the double was itself the result of arithmetic it may already carry IEEE 754 error. When in doubt, construct from String.
Int / long constructors are safe

Unlike double, int and long represent integers exactly, so new BigDecimal(42) is fine. The danger is specifically with float and double.

BigDecimal is immutable. Every arithmetic method returns a new instance; it never mutates the receiver. This means:

BigDecimal a = new BigDecimal("10.00"); a.add(new BigDecimal("5.00")); // result discarded! System.out.println(a); // 10.00 — unchanged // Must assign the result: a = a.add(new BigDecimal("5.00")); System.out.println(a); // 15.00 A common bug: calling total.add(item) and forgetting to reassign. The compiler won't warn you — the result is simply thrown away. Treat BigDecimal like String: always use the return value.
Core operations
import java.math.BigDecimal; BigDecimal x = new BigDecimal("12.50"); BigDecimal y = new BigDecimal("3.20"); BigDecimal sum = x.add(y); // 15.70 BigDecimal diff = x.subtract(y); // 9.30 BigDecimal product = x.multiply(y); // 40.0000 (scales add up!) BigDecimal quot = x.divide(y, 4, java.math.RoundingMode.HALF_UP); // 3.9063 BigDecimal rem = x.remainder(y); // 2.90 BigDecimal squared = x.pow(2); // 156.2500 BigDecimal neg = x.negate(); // -12.50 BigDecimal abs = neg.abs(); // 12.50 Note multiply's scale: 12.50 (scale 2) × 3.20 (scale 2) = 40.0000 (scale 4). Scales add on multiply and subtract on divide. If you want a fixed scale back, call setScale afterwards (covered next).
Method chaining — a worked invoice example

Because every method returns a new BigDecimal, you can chain fluently. Here's a realistic invoice total with line items, a 10% discount, and 8.25% sales tax:

import java.math.BigDecimal; import java.math.RoundingMode; public class Invoice { public static void main(String[] args) { BigDecimal item1 = new BigDecimal("49.99").multiply(new BigDecimal("2")); // 2 × $49.99 BigDecimal item2 = new BigDecimal("19.95"); BigDecimal item3 = new BigDecimal("7.49").multiply(new BigDecimal("4")); // 4 × $7.49 BigDecimal subtotal = item1.add(item2).add(item3); // 99.98 + 19.95 + 29.96 = 149.89 BigDecimal discount = subtotal.multiply(new BigDecimal("0.10")) .setScale(2, RoundingMode.HALF_UP); // 14.99 BigDecimal afterDiscount = subtotal.subtract(discount); // 134.90 BigDecimal tax = afterDiscount.multiply(new BigDecimal("0.0825")) .setScale(2, RoundingMode.HALF_UP); // 11.13 BigDecimal total = afterDiscount.add(tax); // 146.03 System.out.println("Subtotal: $" + subtotal); System.out.println("Discount: -$" + discount); System.out.println("Tax: $" + tax); System.out.println("Total: $" + total); } }

Each step returns a new object; the chain reads naturally. Notice we call setScale(2, HALF_UP) after multiplication to snap back to two decimal places — which brings us to the heart of BigDecimal: scale and rounding.

Two concepts confuse newcomers: scale and precision. They are not the same thing.

BigDecimal n = new BigDecimal("123.4500"); System.out.println(n.scale()); // 4 System.out.println(n.precision()); // 7 System.out.println(n.unscaledValue()); // 1234500 (the digits as a BigInteger)

Internally, a BigDecimal stores an unscaled BigInteger and an int scale. The value is unscaledValue × 10-scale. That's why it represents decimal fractions exactly: it's literally a base-10 integer plus a power-of-ten divisor.

setScale — the workhorse
import java.math.BigDecimal; import java.math.RoundingMode; BigDecimal n = new BigDecimal("2.7182818284"); n.setScale(2, RoundingMode.HALF_UP); // 2.72 n.setScale(4, RoundingMode.HALF_UP); // 2.7183 n.setScale(6, RoundingMode.DOWN); // 2.718281 n.setScale(10); // 2.7182818284 (no rounding needed) n.setScale(2); // ArithmeticException — rounding needed, no mode supplied
The 8 RoundingMode options

Every RoundingMode controls what happens when discarded digits would otherwise change the result. Here's the complete table, showing each mode applied to 2.5 and 2.55 rounded to one decimal place less:

Mode Meaning 2.5 → 0 dp 2.55 → 1 dp -2.5 → 0 dp
UPAway from zero32.6-3
DOWNToward zero (truncate)22.5-2
CEILINGToward +∞32.6-2
FLOORToward −∞22.5-3
HALF_UPNearest; ties go away from zero32.6-3
HALF_DOWNNearest; ties go toward zero22.5-2
HALF_EVENBanker's rounding; ties go to even neighbor22.6-2
UNNECESSARYAssert no rounding needed; throws otherwise
Why HALF_EVEN is the IEEE 754 default. If you always round halves up, you introduce a statistical bias toward larger numbers in any long sum. HALF_EVEN (banker's rounding) alternates — 2.5 rounds down to 2, 3.5 rounds up to 4 — so errors cancel out over many operations. This is why most financial regulators and standards specify it.
Seeing the modes in action
import java.math.BigDecimal; import java.math.RoundingMode; BigDecimal half = new BigDecimal("2.5"); for (RoundingMode mode : RoundingMode.values()) { if (mode == RoundingMode.UNNECESSARY) continue; System.out.printf("%-12s %s%n", mode, half.setScale(0, mode)); } // UP 3 // DOWN 2 // CEILING 3 // FLOOR 2 // HALF_UP 3 // HALF_DOWN 2 // HALF_EVEN 2 Don't use deprecated BigDecimal.ROUND_* int constants. Pre-Java 9 code used integer constants like BigDecimal.ROUND_HALF_UP. These are deprecated — always use the RoundingMode enum instead.

Division is where most BigDecimal bugs live. Because BigDecimal stores exact base-10 fractions, it cannot represent results like 1/3 = 0.3333… — the digits repeat forever. So when you ask it to do impossible division without guidance, it throws.

ArithmeticException: Non-terminating decimal expansion. The one-argument divide(BigDecimal) method only works when the quotient has a finite decimal representation. Otherwise it throws: BigDecimal a = new BigDecimal("1"); BigDecimal b = new BigDecimal("3"); BigDecimal c = a.divide(b); // Exception in thread "main" java.lang.ArithmeticException: // Non-terminating decimal expansion; no exact representable decimal result. The fix is to always specify a scale and RoundingMode: BigDecimal c = a.divide(b, 10, RoundingMode.HALF_UP); // 0.3333333333
All the divide overloads
import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; BigDecimal a = new BigDecimal("10.00"); BigDecimal b = new BigDecimal("3"); // 1. scale + mode — most common a.divide(b, 4, RoundingMode.HALF_UP); // 3.3333 // 2. MathContext — precision-based rounding a.divide(b, new MathContext(6)); // 3.33333 (6 significant digits) a.divide(b, MathContext.DECIMAL64); // IEEE 754 double-equivalent precision a.divide(b, MathContext.DECIMAL128); // quad-precision // 3. divideAndRemainder — returns [quotient, remainder] BigDecimal[] qr = a.divideAndRemainder(b); // qr[0] = 3, qr[1] = 1.00 // 4. divideToIntegralValue — integer part of quotient a.divideToIntegralValue(b); // 3
Practical: splitting revenue between partners

Imagine you need to split $100.00 between three partners. You can't give each $33.3333…, and rounding each share to $33.33 loses a penny. The standard accounting fix is to give the remainder to one partner:

import java.math.BigDecimal; import java.math.RoundingMode; public class RevenueSplit { public static void main(String[] args) { BigDecimal total = new BigDecimal("100.00"); BigDecimal partners = new BigDecimal("3"); // Each partner's fair share, rounded down to cents BigDecimal share = total.divide(partners, 2, RoundingMode.DOWN); // 33.33 BigDecimal distributed = share.multiply(partners); // 99.99 BigDecimal remainder = total.subtract(distributed); // 0.01 System.out.println("Partner A: $" + share.add(remainder)); // 33.34 System.out.println("Partner B: $" + share); // 33.33 System.out.println("Partner C: $" + share); // 33.33 // Total distributed = 100.00 exactly } } The principle: never let money disappear. Always compute the rounded shares, sum them, and route the rounding residue somewhere explicit. Regulators call this "largest-remainder" or "residual" allocation.

Comparing BigDecimals looks innocent but hides a famous gotcha that has broken more financial test suites than any other.

equals() also checks scale. Two BigDecimals are only equals if they have the same value and the same scale. 2.0 and 2.00 are mathematically identical but have different scales, so: BigDecimal x = new BigDecimal("2.0"); BigDecimal y = new BigDecimal("2.00"); System.out.println(x.equals(y)); // false (!!!) System.out.println(x.compareTo(y)); // 0 (equal numerically) System.out.println(x.compareTo(y) == 0); // true Rule: for numeric equality, always use compareTo(...) == 0. Reserve equals for cases where you specifically care that two values have identical representations (rare).
Ordering and sorting
import java.math.BigDecimal; import java.util.*; List<BigDecimal> prices = new ArrayList<>(List.of( new BigDecimal("19.99"), new BigDecimal("4.50"), new BigDecimal("100.00"), new BigDecimal("4.5") // same value as 4.50, different scale )); prices.sort(Comparator.naturalOrder()); // [4.50, 4.5, 19.99, 100.00] — 4.50 and 4.5 are adjacent (compareTo treats them equal) BigDecimal smallest = Collections.min(prices); // 4.50 BigDecimal biggest = Collections.max(prices); // 100.00
Built-in min / max
BigDecimal a = new BigDecimal("10.50"); BigDecimal b = new BigDecimal("10.49"); BigDecimal lower = a.min(b); // 10.49 BigDecimal higher = a.max(b); // 10.50 // Common pattern: clamp to a minimum BigDecimal price = userInput.max(BigDecimal.ZERO); // never go negative Never put BigDecimals in a HashSet or as HashMap keys expecting value-based lookup. HashSet uses equals, so 2.0 and 2.00 are stored as separate entries. If you need value-based uniqueness, either normalize to a canonical scale via setScale first, or use a TreeSet with Comparator.naturalOrder() (which uses compareTo).

BigInteger is BigDecimal's simpler cousin — arbitrary-precision whole numbers, no decimal point, no scale. It shines wherever long's 64-bit ceiling isn't enough: factorials, combinatorics, and especially cryptography.

Creating and basic arithmetic
import java.math.BigInteger; BigInteger a = new BigInteger("123456789012345678901234567890"); BigInteger b = BigInteger.valueOf(1_000_000_000L); BigInteger z = BigInteger.ZERO; BigInteger o = BigInteger.ONE; BigInteger t = BigInteger.TEN; BigInteger two = BigInteger.TWO; // Java 9+ a.add(b); // 123456789012345678902234567890 a.subtract(b); // 123456789012345678900234567890 a.multiply(b); // 123456789012345678901234567890000000000 a.divide(b); // 123456789012345678901 (integer division) a.mod(b); // 234567890 a.pow(5); // enormous a.gcd(b); // greatest common divisor a.negate().abs(); // absolute value
Modular arithmetic for cryptography

RSA, Diffie-Hellman, and most public-key crypto rely on modular exponentiation — raising a number to a large power and taking the result mod some prime. Naively that would blow up to astronomical sizes, but modPow computes it efficiently in place:

import java.math.BigInteger; import java.security.SecureRandom; public class MiniRSA { public static void main(String[] args) { SecureRandom rng = new SecureRandom(); // Generate two 1024-bit primes BigInteger p = BigInteger.probablePrime(1024, rng); BigInteger q = BigInteger.probablePrime(1024, rng); BigInteger n = p.multiply(q); // modulus BigInteger phi = p.subtract(BigInteger.ONE) .multiply(q.subtract(BigInteger.ONE)); BigInteger e = BigInteger.valueOf(65537); // public exponent BigInteger d = e.modInverse(phi); // private exponent BigInteger msg = new BigInteger("42"); BigInteger encrypted = msg.modPow(e, n); // c = m^e mod n BigInteger decrypted = encrypted.modPow(d, n); // m = c^d mod n System.out.println(decrypted); // 42 } } modPow, modInverse, and probablePrime are the three primitives behind most public-key cryptography. They're implemented in heavily optimized native code — you can build a toy RSA in a dozen lines, which is both empowering and a reminder that you should never actually use your own crypto in production.
Bit operations
import java.math.BigInteger; BigInteger n = BigInteger.valueOf(0b1010_1100); // 172 n.bitLength(); // 8 n.bitCount(); // 4 (popcount) n.testBit(2); // true (bit 2 is set in 172) n.setBit(0); // 173 n.clearBit(5); // 140 n.flipBit(7); // 44 n.shiftLeft(3); // 1376 (172 × 8) n.shiftRight(2); // 43 (172 / 4) n.and(BigInteger.valueOf(0xFF)); // 172 n.or(BigInteger.valueOf(0x100)); // 428 n.xor(BigInteger.valueOf(0xFF)); // 83 n.not(); // -173
Prime testing
import java.math.BigInteger; import java.security.SecureRandom; BigInteger n = new BigInteger("104729"); n.isProbablePrime(20); // true — certainty 1 - (1/2)^20 // Generate a random probable prime of a given bit length BigInteger p = BigInteger.probablePrime(256, new SecureRandom()); // Find the next probable prime >= n BigInteger next = n.nextProbablePrime(); // 104743 isProbablePrime(certainty) uses Miller-Rabin. A certainty of 20 gives you a false-positive probability of less than one in a million; 100 makes it astronomically small. For cryptographic use, a certainty of 80–100 is standard.

Displaying BigDecimals to users is not as simple as calling toString(). There are three string methods, each with different behavior:

import java.math.BigDecimal; BigDecimal small = new BigDecimal("0.0000123"); BigDecimal huge = new BigDecimal("123000000"); small.toString(); // "1.23E-5" (scientific!) small.toPlainString(); // "0.0000123" (no exponent) small.toEngineeringString(); // "12.3E-6" (engineering notation) huge.toString(); // "123000000" huge.toPlainString(); // "123000000" huge.toEngineeringString(); // "123E+6" toString() uses scientific notation for small or very large numbers. If you're writing BigDecimals to JSON, CSVs, or databases, always use toPlainString() — otherwise "1.23E-5" will confuse downstream parsers.
The stripTrailingZeros display gotcha
BigDecimal n = new BigDecimal("100.000"); BigDecimal stripped = n.stripTrailingZeros(); System.out.println(stripped.toString()); // 1E+2 (WAT!) System.out.println(stripped.toPlainString()); // 100 (fine)

stripTrailingZeros returns 100 with scale -2, which prints as "1E+2" in scientific notation. Always pair it with toPlainString(), or use setScale(0) if you want a specific result.

Locale-aware currency formatting
import java.math.BigDecimal; import java.text.NumberFormat; import java.util.Locale; BigDecimal amount = new BigDecimal("1234567.89"); NumberFormat us = NumberFormat.getCurrencyInstance(Locale.US); NumberFormat de = NumberFormat.getCurrencyInstance(Locale.GERMANY); NumberFormat jp = NumberFormat.getCurrencyInstance(Locale.JAPAN); System.out.println(us.format(amount)); // $1,234,567.89 System.out.println(de.format(amount)); // 1.234.567,89 € System.out.println(jp.format(amount)); // ¥1,234,568 (yen has no fractional unit)
Custom patterns with DecimalFormat
import java.math.BigDecimal; import java.text.DecimalFormat; BigDecimal n = new BigDecimal("1234567.8"); DecimalFormat money = new DecimalFormat("#,##0.00"); DecimalFormat percent = new DecimalFormat("0.00%"); DecimalFormat scientific = new DecimalFormat("0.###E0"); System.out.println(money.format(n)); // 1,234,567.80 System.out.println(percent.format(new BigDecimal("0.0825"))); // 8.25% System.out.println(scientific.format(n)); // 1.235E6 DecimalFormat accepts BigDecimal directly but internally may route through double for some patterns. For guaranteed precision in output, configure it with setParseBigDecimal(true) and when extreme precision matters, do your own digit formatting using toPlainString() and string manipulation.
Parsing user input safely
import java.math.BigDecimal; public static BigDecimal parseAmount(String input) { if (input == null) return null; String cleaned = input.trim().replace(",", "").replace("$", ""); try { return new BigDecimal(cleaned); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid amount: " + input, e); } } parseAmount("$1,234.56"); // 1234.56 parseAmount(" 42.00 "); // 42.00 parseAmount("abc"); // IllegalArgumentException

Correctness has a price. BigDecimal arithmetic is roughly 50–100× slower than double and uses much more memory — each operation allocates a new object, stresses the garbage collector, and takes many instructions instead of one. For most applications that cost is negligible, but in hot loops or high-frequency trading it can dominate.

When each representation makes sense
Representation Precision Speed Memory Best for Avoid for
double ~15–17 sig digits, binary Fastest (1×) 8 bytes Scientific calc, graphics, ML, physics Money, currencies, exact decimals
long as cents Exact, fixed 2 dp Very fast (~1×) 8 bytes High-volume money within one currency Tax math, multi-currency, division
BigDecimal Arbitrary, exact Slow (~50–100×) ~40+ bytes General money, accounting, tax, invoices Inner loops of number-crunching code
BigInteger Arbitrary integer Slow (grows with size) Varies Crypto, combinatorics, huge counters Decimals, normal counting
The "long cents" pattern

For very high-throughput systems (ad tech, exchanges) you can sidestep BigDecimal entirely by storing money as an integer count of the smallest unit — pennies, satoshis, millicents, whatever. All arithmetic is then primitive long addition, blazingly fast:

public class Money { private final long cents; private Money(long cents) { this.cents = cents; } public static Money dollars(long d) { return new Money(d * 100L); } public static Money ofCents(long c) { return new Money(c); } public Money plus(Money other) { return new Money(this.cents + other.cents); } public Money times(int n) { return new Money(this.cents * n); } @Override public String toString() { return String.format("$%d.%02d", cents / 100, Math.abs(cents % 100)); } } Money a = Money.dollars(10); // $10.00 Money b = Money.ofCents(250); // $2.50 System.out.println(a.plus(b)); // $12.50 System.out.println(a.times(3)); // $30.00 The "long cents" pattern breaks down for tax and interest math: multiplying by 0.0825 needs fractional cents to stay exact. Either drop back to BigDecimal for the computation and convert, or store amounts in sub-cents (e.g., 1/10,000 of a currency unit — the same approach used by Oracle's NUMERIC(19, 4)).
Performance tips if you stay with BigDecimal

Here's how BigDecimal and BigInteger actually show up in production Java applications.

The Money class pattern

Raw BigDecimal has no concept of currency — $10 and €10 look the same. Wrap it in a domain class to enforce type safety and centralize rounding rules:

import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Currency; import java.util.Objects; public final class Money { private final BigDecimal amount; private final Currency currency; private Money(BigDecimal amount, Currency currency) { this.amount = Objects.requireNonNull(amount) .setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN); this.currency = Objects.requireNonNull(currency); } public static Money of(String amount, String currencyCode) { return new Money(new BigDecimal(amount), Currency.getInstance(currencyCode)); } public Money plus(Money other) { requireSameCurrency(other); return new Money(this.amount.add(other.amount), currency); } public Money minus(Money other) { requireSameCurrency(other); return new Money(this.amount.subtract(other.amount), currency); } public Money times(BigDecimal factor) { return new Money(this.amount.multiply(factor), currency); } public boolean isGreaterThan(Money other) { requireSameCurrency(other); return this.amount.compareTo(other.amount) > 0; } private void requireSameCurrency(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException( "Currency mismatch: " + currency + " vs " + other.currency); } } @Override public boolean equals(Object o) { if (!(o instanceof Money m)) return false; return amount.compareTo(m.amount) == 0 && currency.equals(m.currency); } @Override public int hashCode() { return Objects.hash(amount.stripTrailingZeros(), currency); } @Override public String toString() { return currency.getSymbol() + amount.toPlainString(); } } // Usage Money invoice = Money.of("149.89", "USD") .times(new BigDecimal("1.0825")); // + 8.25% tax // $162.25 (rounded HALF_EVEN to 2 dp) Notice the equals method uses compareTo (not BigDecimal's own equals), and hashCode normalizes via stripTrailingZeros to stay consistent with equals. This is the only safe way to put Money in a HashMap.
JPA / Hibernate mapping
import jakarta.persistence.Column; import jakarta.persistence.Entity; import java.math.BigDecimal; @Entity public class Invoice { // precision = total digits, scale = digits after decimal // 19 total, 4 after decimal — industry standard for money with sub-cent accuracy @Column(precision = 19, scale = 4, nullable = false) private BigDecimal subtotal; @Column(precision = 19, scale = 4, nullable = false) private BigDecimal tax; @Column(precision = 19, scale = 4, nullable = false) private BigDecimal total; }

On the database side, this maps to NUMERIC(19, 4) in PostgreSQL/Oracle or DECIMAL(19, 4) in MySQL/SQL Server. Never use DOUBLE, FLOAT, or REAL for money columns — they carry the same IEEE 754 error into your database.

Jackson JSON serialization
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature; ObjectMapper mapper = new ObjectMapper(); // Write full plain-string form (no scientific notation) mapper.enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); // Preserve BigDecimal precision on parse — don't silently convert to double mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); Without WRITE_BIGDECIMAL_AS_PLAIN, Jackson may emit "0.0000123" as "1.23E-5", breaking downstream JSON consumers that expect plain numbers. Always enable both flags for financial APIs.
The tax multiplication pitfall
Never multiply BigDecimal by a double literal. // WRONG — 0.08 becomes 0.080000000000000002081668... BigDecimal tax = amount.multiply(BigDecimal.valueOf(0.08)); // RIGHT — exact 0.08 BigDecimal tax = amount.multiply(new BigDecimal("0.08")); // Also RIGHT — declare it as a constant private static final BigDecimal TAX_RATE = new BigDecimal("0.08"); BigDecimal tax = amount.multiply(TAX_RATE); BigDecimal.valueOf(0.08) actually produces 0.08 correctly (via Double.toString), but new BigDecimal(0.08) does not. The safest habit is to always type decimal rates as string literals.
Best practices summary

Get these habits right and your financial code will be boring — which, when money is on the line, is exactly what you want.