Skip to content

[Ruby] On using BigDecimal

Anh-Tho Chuong edited this page Jan 17, 2023 · 3 revisions

We’ve recently gone deep down the rabbit hole of “Big Decimal”. 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 : 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

Rounding Floats can lead to errors. Capture d’écran 2023-01-17 à 09 56 48

Execution time for a Float calculation Capture d’écran 2023-01-17 à 09 57 53

Execution Time for a BigDecimal calculation Capture d’écran 2023-01-17 à 09 58 21

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.

Clone this wiki locally