Representing money at scale is hard (part 1 of 2)
Authors: George Witteman and Rohan Ramchand
A common line of advice about money in computing goes like this:
Always use integers. Floating points are imprecise.
Therefore, always pass around money in cents as an integer.
Early on at Neon, we had to build an API that took in a money argument (to create a checkout). Having heard this advice, we made the price field an integer representing the price of an item in cents. So, for example, to price an item at USD$1.99, you’d pass in “price”: 199.
This made a lot of things pretty easy:
Most other money-taking APIs worked the same way. Stripe, for example, also took money amounts in cents. So, with a few exceptions (notably PayPal), we could take the amount from the API call, store it in our database, and call Stripe with it with no intermediate steps.
Displaying amounts was only slightly more complex. JavaScript’s NumberFormat API takes in amounts in dollars, so we had to divide all of our amounts by 100 to get things to work. Still, though, pretty easy.
This continued to work great as we expanded into Canada, the UK, and the EU. All we had to do was store a different value in the currency field; everything else worked the same.
Going abroad
Then we decided to expand into Japan. Japan’s currency, yen, was different from every other currency we supported at the time. Whereas currencies like US dollars or UK pounds sterling could all be subdivided into smaller units (cents or pence, respectively), yen couldn’t be subdivided. So, whereas USD$0.01 was the smallest value we could support, JPY bottomed out at JPY¥1. In common terms, the US dollar is expressed in its minor units, whereas the Japanese yen is not.
We now had a problem: we needed to expand our current system to handle currencies without minor units. We were on a tight timeline, so we picked the easiest option: treat everything like USD—i.e., like it had two decimal places. So, for example, USD$1.23 would continue to be represented as 123, and JPY¥123 would be represented as 12300.
This made a lot of things very easy. Our display logic could continue to be “divide by 100” everywhere. However, our Stripe logic now required some special-casing—in particular, we needed to divide JPY amounts by 100, but leave USD and other amounts unchanged. This felt pretty minor, so we wrote a little utility to handle this for us and considered the problem solved.
If you have any experience with handling money programmatically, this may have set off a bunch of alarm bells. And for good reason! This is not a very sound approach. Ideally, a thing’s representation should encapsulate not just what it can be, but also what it can’t. This representation allowed all valid values of JPY, but critically, it also allowed a bunch of invalid values. The value JPY¥1.23 is invalid, but the representation we chose happily allowed { “price”: 123, “currency”: “JPY” }.
At first, things continued to mostly work. All of the JPY amounts in our system were correctly rounded to the nearest 100, and because we called all of the relevant external APIs after dividing by 100 (e.g. 123 instead of 12300), even percentage-based calculations like taxes rounded correctly. There were a few cracks around the edges—in particular, it became more and more unwieldy to make sure we called the “divide-by-100-if-JPY” logic everywhere it was needed.
Promo codes were the final straw. To calculate a 10% discount on a JPY¥123 item, we first needed to divide our stored amount by 100 (to get 123), then calculate the discount (12.3), round to the nearest whole number (12), subtract the discount from the original amount (111), and then finally multiply the new subtotal by 100 (11100). The same logic had to be applied when removing discount codes and recalculating taxes with discounted checkouts. If we ever got this wrong, or worse, if we ever became inconsistent about our rounding, we risked our data becoming untrustworthy—a massive red flag for any company taking payments.
It was clear that the current system wasn’t working. We needed something smarter than just “maybe divide by 100 sometimes.” So what should we do?
Minor units
One problem with our approach is that it more or less assumed the following:
All currencies can be divided into two categories: those that have a minor unit (USD, etc), and those that don’t (JPY, etc).
Of those that do have a minor unit, those minor units represent 1/100th of the major unit (e.g. one dollar equals 100 cents, etc).
Unfortunately, this isn’t true. ISO 4217, the official standards document for currency codes, lists the 179 official currencies used by 280 countries, territories, and other various geopolitical phenomena (Antarctica, etc). Each currency lists a “minor unit” field. But, contrary to the assumption we made above, this isn’t a boolean field—it’s an integer, representing the number of digits left of the decimal point the currency allows. Put differently, taken as a power of 10, it’s the amount of minor units that make up one major unit. Borrowing slightly from database terminology, let’s call this value the “scale” of the currency.
For example, USD, GBP, and 152 other currencies have a scale of 2, meaning the major unit equals 10^2 = 100 of the minor unit. JPY and 16 other currencies have a scale of 0, meaning that their major units are made up of 10^0 = 1 of the minor unit, or alternatively that they don’t have a minor unit at all. The rest are either 3- or 4-scale.

(Note that this concept is sometimes referred to as an “N-decimal currency,” as in “USD is a 2-decimal currency” or “KRW is a 0-decimal currency.” The N is what we're calling the scale.)
Plus, crypto: Blockchains can in theory support arbitrarily precise amounts, but in practice tend to pick a minor unit, presumably because floating point math on a blockchain is no less error-prone than floating point math anywhere else. Both Bitcoin and Ethereum have minor units (the 8-scale Satoshi and 21-scale wei, respectively). If we wanted to support crypto, we would need to handle these much more precise currencies as accurately as we handle the less precise ones.
And so
A lesson you could reasonably draw from all of the above is as follows:
Money looks arbitrarily precise, but it isn’t always. Until you actually need to move money in the real world (e.g. by charging a user), you can represent money internally however you want.
However, once you do need to move the money around, you have to round it to the currency's minor unit.
Thus, in order to do anything with money, you can’t just know the amount: you also need to know the scale of the currency.
So, we can represent money as an object with two fields: an amount and a currency, the latter of which determines the scale.
Once we realized this, the next step was to figure out how to represent all of this in our system. Ideally, the solution we came up with would be both easy to understand, easy to use, and extensible to any currency—no matter how esoteric—in the world.
In the next post, we’ll discuss exactly how we went about doing this: why none of the third-party libraries we found worked exactly right for us, what we decided to build, and how we moved everything over.