How to store and handle monetary values?
I once worked on a fintech project where monetary values were stored however they wished - as text, decimals, or even floats (scary). I’ll spare you the details of this system, but it still lives in my head and ruins the quality of my sleep.
I spent a lot of time fighting with LLMs that were trying to overengineer a solution to this problem. I’m writing this to save you time, because the rules are pretty straightforward.
The floating-point trap
To start off, here is how you should never store monetary values: floats or doubles.
Here is a textbook example: how much is 0.1 + 0.2?
It depends on who you ask.
// JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004
# Python
print(0.1 + 0.2) # 0.30000000000000004
// Rust
println!("{:.17}", 0.1_f64 + 0.2_f64); // 0.30000000000000004
Nice! They all agree - the problem is, it’s not true.
These types are just approximations. This might be obvious to some, but I didn’t know it for the first 4 years of my professional SWE career, so let that sink in!
The outcome is the same because all of them are based on the IEEE 754 standard: a number is stored as a sign bit, an exponent, and a mantissa (the significant digits). A float (often called single precision because it’s 32 bits) gives you ~7 significant decimal digits of precision, and a double (because it’s 64 bits) gives you ~15-16.
Option 1: Store as integers (cents)
If you are in the design phase, just pick integers. For example, store $9.99 as 999 (cents).
It will make your life so much easier. Leading fintech companies use this pattern, and it’s proven to scale very well (e.g., Stripe).
If you go global, be careful because not all currencies follow the “100 cents = 1 dollar” rule. However, handling that is still much less overhead than dealing with floating-point inaccuracies.
Option 2: Use NUMERIC or DECIMAL
This is a second good option. It requires less refactoring, so in my opinion, it’s better for systems already in production (but it’s not a rule, your system may be different!).
NUMERIC and DECIMAL are literally the same thing - different database vendors just called it differently before any common standard was established.
This type allows you to choose your granularity (how many digits you want to keep).
For example, NUMERIC(10, 2) allows you to store up to 10 digits in total, with 2 digits after the decimal point. So in theory, it allows you to store up to 10^8 (100,000,000).
Both NUMERIC and integer types solve the precision loss problem. But there is a catch if you are using JavaScript.
The JavaScript trap
Let’s say your database table stores price and shipping_cost (both as NUMERIC). You want to calculate the total price before billing the customer.
| product_id | price | shipping_cost |
|---|---|---|
| 123 | 99 | 10.99 |
const product = await client.getProduct(123);
const totalPrice = product.price + product.shipping_cost;
await userBalanceService.charge(123, totalPrice);
console.log(totalPrice); // "9910.99"
We just overcharged the user by $9 801!
Why did this happen? The database driver returned string representations of the numbers.
Howerver it’s not the database fault. JavaScript only has one number type - number, which is a 64-bit float (double). As we discussed earlier, it only has ~15-16 digits of precision. However, in PostgreSQL, NUMERIC can store up to 131,072 digits before the decimal point. Since there is no safe way to fit that into a standard JavaScript number, database drivers return a string to preserve the exact value.
And JavaScript is pretty liberal about string arithmetic, so "99" + "10.99" is "9910.99".
How to fix it
We could manually parse it and write our own wrappers. But unless you are counting every single kilobyte of your bundle size, there is no need to reinvent the wheel. There are already a couple of lightweight, battle-tested npm packages that solved this issue years ago - like decimal.js, big.js, or bignumber.js.
Personally, I like decimal.js.
const product = await client.getProduct(123);
const totalPrice = new Decimal(product.price).plus(product.shipping_cost);
await userBalanceService.charge(123, totalPrice.toString());
console.log(totalPrice.toString()); // '109.99'
TL;DR
- NEVER store monetary values as floats or doubles.
- Use
NUMERIC/DECIMALin your database, or use integers (storing cents instead of dollars). - If you use
NUMERIC/DECIMALwith JavaScript, watch out for string concatenation and always wrap your math operations using a library likedecimal.js.