Supposedly Bill Gates, circa 1981.
Recently at TigerBeetle, we’ve decided to use 128bit integers to store all financial amounts and balances, retiring our previous use of 64bit integers. While some may argue that a 64bit integer, which can store integers ranging from zero to 2^{64}, is enough to count the grains of sand on Earth, we realized we need to go beyond this limit if we want to be able to store all kinds of transactions adequately. Let’s find out why.
How do we represent money?
To represent numbers (and to be able to do math with them), computers need to encode this number in a binary system which, depending on the range and the kind of number, requires a certain amount of bits (each bit can be either 0 or 1). For example, integers (whole numbers) ranging from 128 to 127 can be represented with only 8 bits, but if we don’t need negative numbers, we can use the same bits to represent any integer from 0 to 255, and that’s a byte! Larger numbers require more bits, for example, 16bit, 32bit, and 64bit numbers are the most common.
You may have noticed that we are talking about money as whole numbers and not as decimal numbers or cents. Things get more complicated with fractional numbers, which can be encoded using floating point numbers. While binary floating point may be fine for other calculations, they cannot accurately express decimal numbers. This is the same kind of problem that we humans have when we try to represent ⅓ in decimal as 0.33333…, computers have to represent ¹⁄₁₀ in binary!
>>> 1.0 / 10
0.10000000000000001
As “fractions of a penny” add up over time to a lot, floating
point is a
disaster for finance!
Therefore, in TigerBeetle, we don’t use fractional or decimal numbers, every ledger is expressed as multiples of a minimal integer factor defined by the user. For example, you can represent Dollars as a multiple of cents, and then a $1.00 transaction can be described as 100 cents. Even nondecimal currency systems can be better represented as a multiple of a common factor.
Surprisingly, we also don’t use negative numbers (you may have encountered software ledgers that store only a single positive/negative balance). Instead, we keep two separate strictly positive integer amounts: one for debits and another for credits. This not only avoids the burden of dealing with negative numbers (such as the myriad of languagespecific wraparound consequences of overflow… or underflow), but most of all preserves information by showing the volume of transactions with respect to everincreasing balances for both the debit and credit sides. When you need to take the net balance, the two balances can be subtracted accordingly and the net displayed as a single positive or negative number.
So, why do we need 128bit integers?
Back to the example of representing $1.00 as 100 cents. In this case, 64bit integers can count to something close to 184.5 quadrillion dollars. While it may not be an issue for many people, the upper limit of a 64bit integer becomes restrictive when there is a need to represent values smaller than a cent. Adding more decimal places dramatically reduces this range.
For the same reason, digital currencies are another use case for 128bit balances, where again, the smallest quantity of money can be represented on the order of microcents (10^{6})… or even smaller. Although it’s a compelling use case for TigerBeetle to support, we found a variety of other applications that also benefit from 128bit balances.
Let’s think some more about scenarios where $0.01 is too big to represent the value of something.
For example, in many countries, the price of a gallon/liter of gasoline requires three digits after the decimal point, and stock markets already require pricing increments of hundredths of cents like 0.0001.
Or, in an economy of highfrequency micropayments, greater precision and scale are also required. Sticking with 64bit values would impose artificial limits on realworld demands, or force applications to handle different scales of the same currency in separate ledgers, by painstakingly splitting amounts across multiple “Dollar” and “MicroDollar” accounts, only because a single 64bit balance isn’t enough to cover the entire range of precision and scale required for many micropayments to represent a multibillion Dollar deal.
The value of a database that can count well (and at scale) is also not limited to money. TigerBeetle is designed to count not only money, but anything that can be modeled using doubleentry accounting. For instance, to count inventory items, the frequency of API calls, or even kilowatts of electricity. And none of those things need to behave like money or be constrained to the same limits.
Futureproof accounting.
Another thing about the upper limits of amounts and balances, is that, while it may seem unlikely for a single transaction amount to exceed the order of magnitude of trillions or quadrillions, account balances accumulate over time. For longrunning systems, it’s likely that an account could transact such volume over the years, and so then a single transfer must also be able to move this entire balance from one account to another. This was a gotcha we ran into, as we considered whether to move to 128bit transaction amounts and/or only 128bit account balances.
Finally, even the most unexpected events such as hyperinflation can push a currency toward the upper limits of a 64bit integer, requiring it to abandon the cents and cut the zeros that have no practical use.
Can your database schema survive this?
We may not be able to intuit how big a 128bit integer is. Not merely twice the 64bit; it’s actually 2^{64} times bigger! To put this in perspective, a 64bit integer is not enough to handle that One Hundred Trillion Dollar bill if we encode our ledger at a microcent scale. However, using 128bit integers we should be able to perform 1 million transfers per second of the same value for a thousand years and still not hit the account balance limit.
1.000e20 // one hundred trillion at
microcent scale
x 1.000e6 // 1 million transfers per second
x 3.154e7 // the number of seconds in a year
x 1.000e3 // a thousand years

= 3.154e36 // less than 2^128 ≈ 3.4e38
Let’s do some
napkin math!
With BigInteger comes big responsibility.
Modern processor architectures such as x8664 and ARM64 can handle arithmetic operations involving 64bit values, but, if we understand correctly, they don’t always have a specific instruction set for native 128bit calculations. When dealing with 128bit operands, the task may have to be segmented into 64bit portions that the CPU can execute. Consequently, we considered whether 128bit arithmetic may be more demanding compared to the singleinstruction execution possible with 64bit integers.
The table below compares the x86_64 machine code generated for 64bit and 128bit operands. Don’t worry, you don’t need to be an assembly expert to get the point! Just note that the compiler can optimize most operations into a sequence of trivial CPU instructions, such as carry sum and borrowing subtraction. This means that the cost overhead of using 128bit amounts is not material for TigerBeetle.
Operation  64bit operands  128bit operands 
a + b 
mov rax, rdi

mov rax, rdi

a  b 
mov rax, rdi

mov rax, rdi

a * b 
mov rax, rdi

mulx r8, rax, rdi

a / b 
mov rax, rdi

push rax

a == b 
cmp rdi, rsi

xor rsi, rcx

2. 128bit division cannot be expressed as a sequence of 64bit instructions and needs to be implemented by software.
Something else we had to consider as part of this change were all our clients, since TigerBeetle needs to expose its API to many different programming languages that don’t always support 128bit integers. The mainstream languages we provide clients for, currently need to use arbitraryprecision integers (aka BigInteger) to do math with 128bit integers. The sole exception is .Net which recently added support for Int128 and UInt128 data types in .Net 7.0 (kudos to the DotNet team!).
Utilizing BigIntegers comes with additional overhead because they are not handled as fixedsize 128bit values but are instead heapallocated as variablelength byte arrays. Also, arithmetic operations are emulated by software during runtime, which means they can’t take much advantage of the optimizations that would be possible if the compiler knew the kind of number it’s dealing with. Hey, Java, Go, and even C#, I’m looking at you.
To mitigate this cost on the client side (and, of course, to stay true to our TigerStyle), we store and expose all 128bit values (e.g. IDs, amounts, etc.) as just a pair of stackallocated 64bit integers (except for JavaScript, since it does not support 64bit numbers either). Although the programming language has no knowledge of this raw type and can’t perform arithmetic operations on them, we offer a set of helper functions for converting between idiomatic alternatives existing in each ecosystem (e.g., BigInteger, byte array, UUID).
Our API is designed to be nonintrusive, giving each application the freedom to choose between using BigIntegers or handling 128bit values through any thirdparty numerical library that makes the most sense. We want to provide excellent highperformance lowlevel primitives, as far as possible, with a minimum of “sugar”, without taking away from the freedom of the user at a higher layer.
Conclusion
TigerBeetle is designed for a new era where financial transactions are more precise and more frequent. A new era that has already begun and is full of everydaylife examples that 64bit balances ‘ought to be enough!’ for not much longer. To 128bit… and beyond!
TigerBeetle's got a bigger piggy.
— TigerBeetle (@TigerBeetleDB) September 19, 2023
We've moved to 128bit balances.
Here's @rbatiati with the (surprising) reasons why:https://t.co/hhhDF1a3bN pic.twitter.com/rUJNcY6ftC