🥱 TL;DR—Floats are not a safe type for performing money calculations. Don’t use them at all, ever. Integers have limitations that should be considered. Use a dedicated Money value object instead.
When writing software that deals with handling people’s money, precision and accuracy are paramount. You might assume that using native data types like floats and integers to handle money values is sufficient. However, relying on primitive types like this can lead to bugs that are both difficult to detect and have the potential to be very costly.
This blog post discusses an alternative approach to thinking about handling money in your code.
The Problem With Using Float Instances to Represent Money
Floats might seem like the natural choice for handling money values in code. Money values can be easily represented as floats with units on the left of the decimal point and sub-units on the right e.g. price = 19.99 # $19.99
However, when it comes to financial calculations, floats can be unreliable because they often lack precision. Try the following in a Ruby console:
The chances are, this single execution will return a new float that accurately represents the sum of value_a and value_b and has a similar scale (e.g. 2 decimal places). However, if we run this same block of code a few more times, we start to notice that some of the values do not look as we would expect…
Running the above block in Ruby 3.2 reveals an inaccurate result about 20% of the time.
Floating point numbers are notorious for being inaccurate when used in mathematical operations, as demonstrated above. For more information on this, I would encourage you to read the Accuracy Problems section of the Floating-point arithmetic Wikipedia entry.
TL;DR: this is a fundamental problem with how floats are stored in memory, and not an issue that is likely to go away any time soon. Don’t perform money calculations with floats!
The Problem With Using Integer Instances to Represent Money
To avoid the problems with floating point numbers described above, engineers often use integers to represent money values. Money values can be represented as integers equal to the cent value of the money e.g. price = 19_99 # $19.99
Since integers are not prone to the same accuracy errors as floats, this is a reliable way to avoid bugs that are caused by inaccurate arithmetic computations.
However, using integers to represent money amounts can become a problem when the requirements for the business domain change, and new currencies are added to the picture. This is because not all currencies have sub-units that are 1/100th of the value of the primary unit.
For example, Japanese Yen has no sub-unit, whereas the Kuwaiti dinar has a sub-unit that is 1/1000th, rather than 1/100th. Some small countries have currencies that are 1/5th of the main unit. And Bitcoin and other cryptocurrencies have subunits that are 1/100,000,000th the main unit.
Without labouring the point too much, not all currencies can be represented in cent values. This assumption that money can always be represented in 100ths (and many other assumptions about money) can make it difficult to introduce new currencies to a software domain once the software has been built on that assumption. The plausible lifespan of the software and currencies it might represent should be taken into account when deciding how to represent money values in both databases and code.
As well as the issues with currencies and their sub-units, there is also a human factor that must be considered when representing money amounts in cents.
Engineers working with money in sub-units will have an additional cognitive overhead caused by having to perform the conversion of unit to sub-unit mentally in order to understand how a piece of code works. This can also make the code more bug prone, especially at interfaces, and could result in money calculation errors being made at the scale of 100x (e.g. selling something for 40c instead of $40).
Why We Should Use a Money Library
1. Precision and accuracy
To avoid floating-point arithmetic errors, these libraries use fixed-point arithmetic or decimal-based arithmetic to ensure precise representation of currency values (Money uses BigDecimal). These arithmetic models are designed (and well tested) to handle decimal fractions without introducing rounding errors. As a result, calculations are usually accurate and reliable, and the risk of bugs causing financial discrepancies is mitigated.
2. Currency comes included
Money values are not simply a numeral, but a numeric value and a unit. It’s important that we don’t treat money as a mere number in code. For example, very few people would be eager to exchange $100 US Dollars to receive $200 Zimbabwean dollars—even though 200 is twice as many as 100! Currency matters in the context of money, and libraries like Money include currency with every monitory representation. This allows us to perform comparisons and calculations, but with safeguards that ensure we stay within the relevant currency context. For example:
Money also provides the ability to perform currency calculations based on given exchange rates, but that is outside the context of this post.
Suitable money libraries provide a built-in context for each currency, allowing developers to specify the rules for rounding, precision, and formatting. This ensures consistency across all calculations and eliminates the guesswork associated with using floats and integers.
3. Code readability and consistency
By adding a dedicated value object that is specifically designed to represent Money values, we create a central place in the code to define all of the behaviours relevant to money.
Value objects help bring a codebase to life, turning abstract concepts into a definite first-class object within our domain. This moves us closer to what Domain Driven Design advocates call a “deep model”, where our code more closely resembles the business domain that it models. Done properly, a deeper model should help make the code easier to read, as well as DRYer and easier to maintain.
Consider the following code examples:
In the second (”GOOD”) example, the responsibility for presenting the money as a formatted string and for discounting it by a given percentage amount, are both handed internally by the Money class, rather than being defined ad hoc in the current code context. This creates a single point of truth for how these behaviours are defined, and should help to reduce the complexity of the code while reducing the risk of bugs.
4. Regulatory compliance and code quality
One final, but very important consideration on this topic is that of regulatory compliance and code quality standards.
Dealing with other people’s money is serious business, and it’s important that you are able to demonstrate that you understand the risks and considerations when dealing with money calculations, and have taken reasonable steps to mitigate against the risks in this area. There may even be legal and regulatory requirements that expect certain standards are met.
Consistent and judicious use of a trustworthy money library (such as Money) is a good way to demonstrate that you take this issue seriously.
How to store money values
It’s also important to consider storage of money values in your database, as setting the wrong constraints or column types can be limiting and introduce bugs that affect the preciseness of the values being stored. For example, using the wrong constraints can mean that values such as $99.99 are stored as $99.0in your database, which will undoubtedly cause bugs when these incorrect values are loaded from the database.
Typically, the NUMERIC or DECIMAL types are the ideal option for The PostgreSQL documentation describes NUMERIC types as the following:
The type numeric can store numbers with a very large number of digits. It is especially recommended for storing monetary amounts and other quantities where exactness is required. Calculations with numeric values yield exact results where possible, e.g., addition, subtraction, multiplication.
The precision and scale requirements may vary, depending on which currencies should be supported, but these considerations should be discussed when designing your database schema.
When developing software that involves money and financial transactions, the use of suitable money libraries is not a luxury but a necessity.
The potential consequences of relying on floats to represent money can be costly and serious. Using integers comes with its own pitfalls too, and can trap you in a cul de sac that makes future changes more costly and painful.
It’s important that we bear these considerations in mind when developing new features and when reviewing pull requests made by our colleagues.
Where possible, encourage the consistent use of a Money library.