We’ve recently gone deep down the rabbit hole of 'BigDecimal'. Although we accumulated decades of experience in Ruby, this was more challenging than we initially thought, because of the few things we discovered along the way (that led to quite a bit of rework!).
Why we chose BigDecimal
Context: We build Lago, the open-source metering and usage-based billing.
Our need:
We handle objects with up to 5 decimals, without transforming them. Why? We need to keep each object as precise as possible, and delay the rounding to 2 decimal places to the last step: when our users choose to display a charge or an invoice in a specific currency, for instance.
Why didn’t we just store values in decimal? We need to support multiple decimals for complex pricing : e.g. $0,0015 per hour. We want to give the maximum flexibility to our users to fit their pricing needs.
The potential solutions:
BigInt: the ‘by default’ option with Ruby doesn’t support decimals, so we ruled it out.
Float objects: we hit accuracy limits very quickly as float in Ruby (like in many other languages) can lead to rounding issues, it’s designed for performance and not for precise calculations.
BigDecimal: we knew it ranked lower on the performance side, but it seemed to be the only relevant option. Moreover, PostgreSQL provides a Decimal type that is very well integrated with Rails Active Record, which, combined with BigDecimal provides an ‘out-of-the-box’ validation for scale and precision.
Example of a migration: With BigDecimal, we can specify precision and scale in the decimal column (PostgreSQL).
t.decimal :rate_amount, null: false, default: 0, precision: 5
Precision: total number of digits (including before and after the decimal point).
Scale: number of digits after the decimal point.
The problems we ran into
1. Rounding errors and execution time
Float rounding leading to potential errors
Execution time for a Float calculation
Execution Time for the same calculation, using BigDecimal
2. Front-End & Back-End coordination
Handling numbers can be tricky on the front-end side. We either have different types (string, float, integers), format (cents, decimal) or purpose (money, percent). For all of them we also need to change the UI display, depending on currency and user’s locale.
How we solved them
Back-End and Front-End agreed on some rules when dealing with numbers. If a value sent or received contains a decimal (no matter the precision), we’ll manage it as type ‘String’. On the other hand, all other values that won’t contain any decimal will be of type ‘Number’.
The Front-End component libraries can accept both types as inputs, so we don’t have to think about type management when building our interfaces. Internally, for manipulation and formatting, those components will systematically transform values into type ‘String’.
For display purposes, we use Javascript APIs that accept options to define the number of decimals or the display format (currency, percent).
Final words
We shared these notes and code snippets in the simplest form possible, because it’s what we were looking for when we researched the issue. From the questions we spotted in the Ruby community, it looks like other people were confused as well, so we hope this post proves useful to you as well.
Top comments (0)