Every payments codebase has a line like this, written on a quiet afternoon by someone who was perfectly reasonable:
long cents = (amount * 100).longValue()
It compiles. It passes the test with 10.00. It ships. And it quietly takes a cent off a fraction of your payments for as long as it lives.
Bakshish had it in six places. We found them in one audit and removed all of them. Here is what we replaced them with, and two more rules that belong next to it.
Rule 1: round money, never truncate it
longValue() doesn't round. It cuts. Take a 5% platform fee on a 5.55 tip:
- 5.55 x 0.05 = 0.2775
- In cents: 27.75
longValue()says 27. Rounding half up says 28.
One cent. On one payment it is invisible. Over a month, every fee that lands on a fraction goes the same way, always down, always in the same direction. That isn't noise, it's a bias, and it makes your ledger disagree with the payment provider by a little more each day.
The fix is one function that every amount passes through:
amount.setScale(2, RoundingMode.HALF_UP).movePointRight(2).longValueExact()
Two details matter. HALF_UP is explicit, so nobody has to guess which rounding is meant. And longValueExact() throws if the number doesn't fit, instead of silently handing you a wrong one. A loud failure at 3 pm beats a quiet one in the accounts.
Rule 2: the number of decimals is not always two
Our one function takes the currency too. Euros have two decimals. Japanese yen has none: 500 yen is 500, not 50000. Multiply by 100 for everything and a 500-yen tip becomes a 50,000-yen charge.
We run in euros, so this has never happened to us. That is exactly why it belongs in the helper: the day someone adds a currency, the bug shouldn't be waiting for them. A list of zero-decimal currencies and a fractionDigits(currency) is twelve lines.
Rule 3: record a payment exactly once
The third rule isn't about arithmetic. It's about the same payment arriving twice.
A tip is confirmed by the browser, and also announced by the payment provider's webhook, in case the browser never made it back. Both paths end in the same method. Usually one of them wins by a second or two. Sometimes they arrive together.
So recording a payment has to be safe to repeat:
- Look the payment up by its provider ID. If it's already recorded, return that record and stop.
- Otherwise insert it, with a unique index on that ID in the database.
- If two requests race past step 1, the index rejects the loser. The loser retries once, finds the winner and returns it.
The code check is a courtesy. The index is the guarantee. You want the database to say no, because it is the only party that sees every request.
Webhook events get the same treatment: each one is stored by source and event ID, so a provider that re-sends an event, which they all do eventually, can't pay anyone twice.
The takeaway
Money code is code where the boring cases matter. The rules are short:
- Convert in one place, round explicitly, fail loudly.
- Let the currency decide the decimals.
- Make "record this payment" safe to call twice, and let the database enforce it.
None of it is clever. That's the point. Clever is for the parts of the system where a cent doesn't count.
Search your codebase for longValue() today. If it sits next to a price, you have something to read before lunch.
