Currencies and Exchange: Modeling Money Safely. Java Money from a practitioner point of view with lessons you can keep for a long time.
Why talk about money modeling today
The pound just whipsawed after the Brexit vote and every pricing page got nervous. Money bugs do not show up in unit tests. They show up on invoices and chargebacks.
If your code moves money across borders or between wallets you want a model that is boring and safe. Java Money JSR 354 gives us just that.
What goes wrong if you use double
Binary floats cannot represent decimal cents exactly. Tiny errors pile up and a report is off by a cent here and a cent there. Support tickets love that.
Use a decimal type for values that must match a bank statement. In Java that means BigDecimal for arithmetic and a proper money type for behavior.
Is BigDecimal enough by itself
It stores the number but it does not carry the currency, the rounding rules, or the scale you intend. Two amounts with the same digits can mean different things in different currencies.
We need an object that couples amount and currency and guards all operations. That is the core idea behind Java Money.
So what is Java Money JSR 354
It is an API that models monetary amounts, currencies, rounding and exchange. You get interfaces and a reference library named Moneta that you can add to your app today.
The design is simple. MonetaryAmount holds the number plus currency. CurrencyUnit identifies the currency as ISO 4217 or a custom code for points or miles.
How should I represent an amount
Favor immutable money objects. Each operation returns a new value and the old one stays put. That keeps threads calm and intent clear.
When you get input, normalize with a rounding strategy that your business owns and name it. Rounding without a name is a bug waiting for a meeting.
What about currency identity
Currency is part of equality. Ten in USD is not equal to ten in MXN. Build that into your comparisons. Store currency code next to the amount everywhere.
Mind the fraction digits defined by ISO 4217 but do not assume two. JPY has zero. Many Gulf currencies use three. Crypto tokens and loyalty points can use anything.
How do exchange rates fit in
A rate is not a magic number. It needs a base currency, a term currency, a timestamp, a source and a type like spot or cash.
JSR 354 models this with providers and rate types. That lets you say which source priced the conversion and when it was valid. Your audit trail will thank you.
Do rates change over time
Yes and your math should know the date. Converting an invoice from last month needs the rate from that day or from the contract rules. Time makes numbers honest.
Cache rates with a time to live and tag them with the provider. If you ever mix sources by mistake you will catch it in logs because the provider is visible.
How should I round
Pick a rule that matches the business moment. Tax and interest often use bankers rounding. Cash registers may use cash rounding for coins. Fees may always round up.
Do not round early. Keep full precision through internal steps and round only at boundaries like charge, payout or display. One name per rounding rule helps keep this straight.
What about formatting for people
Display is not storage. Format with locale data so the user sees the right digits and separators. CLDR gives you symbols and patterns and the API hooks into that.
Keep the raw amount and the code in your database. The string you show to a person should be a view on top of the real value not the value itself.
How do I persist money
Use two columns. One numeric for the amount and one varchar for the currency code. Match the precision to your highest need and let the app enforce the scale per currency.
If you store minor units as integers make sure the code that multiplies and divides is owned by one place only. Duplication here is a silent fork in the road.
How do I keep arithmetic safe
Only add or subtract amounts with the same currency. Multiply by scalars like rates or percentages using a clear rounding rule. Reject mixed currency math early with loud errors.
Immutability plus clear methods like add, subtract, multiply and divide keep the intent readable. When the code reads like an invoice the bugs have fewer places to hide.
What can go wrong with equals
If you compare raw BigDecimal values, scale can break equality. Ten and ten point zero are not equal by default. A money type can normalize this and keep you out of trouble.
Hash codes should include currency. Collections of prices in different currencies must not collide. This is where a library saves you from tiny traps.
Should I support custom currencies
Yes for store credit, gift cards, or miles. Give them a code and rules for fraction digits and rounding. Keep the prefix different from ISO 4217 so mistakes are obvious.
When converting between these and fiat currencies treat the exchange like any other rate with source, date and rule. That keeps audits simple.
How do I test money code
Write examples with known rate snapshots and known rounding. Test both sides of a boundary like half values and smallest fractions.
Feed the same set of operations in random order and expect the same result. Associativity is not a rumor when amounts and rounds are consistent.
What about fees taxes and discounts
Model them as money with tags for type and rule. Do not hide them inside a rate. When you need to show a breakdown the data is already clean.
Apply them in a defined order and round at the stage that matches your contracts. The last cent should have a reason you can explain to your support team.
Do time zones matter for money
Yes when rates and settlements cross borders. Capture the instant you priced the trade and the local date of the business event. Both can decide which rate applies.
Store everything in UTC with the original zone kept as a separate field. Time is data, not decoration.
A quick checklist you can steal
- Use a money type that couples amount and currency and is immutable
- Never use double for money
- Define named rounding rules for each business step
- Keep rate provider, type and timestamp with every conversion
- Persist amount and currency code in separate columns
- Format for people with locale data not with ad hoc patterns
- Validate currency on every arithmetic operation
- Cache rates with an expiry and log the source
- Test boundaries and reordering of operations
- Treat fees and taxes as first class amounts
Where does this fit with tools you use today
Spring Boot just shipped a fresh release and it plays nicely with Moneta. You can wire a rate provider, expose a price endpoint and keep your services honest about currency.
On the payments side Stripe, PayPal and friends already think in currency tagged amounts. Mirror that model inside your app so the flow stays consistent end to end.
Wrap up
Money bugs are small but expensive. Java Money JSR 354 gives you a vocabulary and guard rails that match how finance actually works.
Hold amount and currency together, name your rounding, log where rates came from and keep time with your numbers. Do these and your prices stay calm even when markets do not.