Penny Perfect: The Hidden Art of Money Representation in Fintech

Ahmed Ghazey
6 min readJun 29, 2024

--

In the fast-paced world of digital finance, the accuracy of every transaction is paramount. Whether you’re a consumer checking your bank balance or a fintech startup processing millions of payments, how money is represented in backend systems can make or break trust, compliance, and even the bottom line.

Imagine a customer's frustration when find a discrepancy in their account or the potential legal nightmare for a company that misreports financial data due to a simple formatting error. These scenarios aren’t just hypothetical — they’re the real risks of improper money handling in digital systems.

So how do we bridge the gap between user expectations, regulatory requirements, and technical limitations? The answer lies in the often-overlooked realm of data types and number formats.

The most intuitive representation of money in computer systems is often floating-point numbers, as they mirror our decimal system. Let’s examine the floating-point representation in computers and evaluate its suitability for financial calculations. We’ll explore basic arithmetic operations — addition, subtraction, multiplication, and division — and assess their accuracy and precision when dealing with monetary values.

The IEEE 754 Floating Point Representation

Let's take a number and demonstrate why the IEEE 754 is not accurate our number is: 263.3

Step 1: Convert the integer part (263) to binary
263 ÷ 2 = 131 remainder 1
131 ÷ 2 = 65 remainder 1
65 ÷ 2 = 32 remainder 1
32 ÷ 2 = 16 remainder 0
16 ÷ 2 = 8 remainder 0
8 ÷ 2 = 4 remainder 0
4 ÷ 2 = 2 remainder 0
2 ÷ 2 = 1 remainder 0
1 ÷ 2 = 0 remainder 1

Reading the remainders from bottom to top, we get:
263 in binary = 100000111
Step 2: Convert the fractional part (0.3) to binary
0.3 × 2 = 0.6 integer part 0
0.6 × 2 = 1.2 integer part 1
0.2 × 2 = 0.4 integer part 0
0.4 × 2 = 0.8 integer part 0
0.8 × 2 = 1.6 integer part 1
0.6 × 2 = 1.2 integer part 1
...
This repeats infinitely.
0.3 in binary ≈ 0.010011001100110011...
Step 3: Combine integer and fractional parts
263.3 in binary ≈ 100000111.010011001100110011...
Step 4: Normalize for IEEE 754 representation
Shift the decimal point left 8 places:
1.00000111010011001100110... × 2^8
Step 5: IEEE 754 representation
Sign bit: 0 (positive)
Exponent: 8 + 127 (bias) = 135 = 10000111 in binary
Fraction: 00000111010011001100110 (23 bits, rounded)
Final IEEE 754 representation:
0 10000111 00000111010011001100110

Floating-point representation has inherent limitations in precision due to its finite storage capacity. With only 23 bits allocated for the fraction in single-precision format, it cannot accurately represent all decimal numbers, especially those with infinitely repeating binary expansions. This constraint leads to rounding errors and approximations.

It’s clear that we need a more suitable alternative. Our focus now shifts to identifying proper representation methods and standardized formats that can minimize errors and enhance calculation precision.

Let’s dive into the ISO formats for money representation and why they matter so much in fintech.

ISO 20022: The Financial Messaging Standard

The International Organization for Standardization (ISO) 20022 is a global standard for financial messaging. It defines a common language for financial transactions, including how to represent monetary amounts. Key aspects include:

  1. Decimal representation: Amounts are typically stored as decimal numbers to avoid rounding errors associated with floating-point representations.
  2. Scale: The number of decimal places is specified, usually 2 for most currencies but can be different (e.g., 3 for Kuwait Dinnar, 0 for Japanese Yen).
  3. Sign: Positive or negative values are clearly indicated.

ISO 4217: The Currency Code Standard

ISO 4217 standard defines three-letter codes for currencies. Examples include:

  1. EGP (Egyptian Pound)
  2. SAR (Saudi Riyal )
  3. USD (United States Dollar)

Using these standardized codes is essential for clear communication between systems and avoiding ambiguity. For instance, “dollar” could refer to US dollars, Canadian dollars, or Australian dollars, while “USD” is unambiguous.

Implementing Money Formats in Backend Systems:

  1. Use a decimal type: Many programming languages offer decimal types that avoid floating-point imprecision.
  2. Store the amount and currency separately: This allows for easier multi-currency operations.
  3. Use the smallest currency unit: Store amounts in cents (or equivalent) to avoid decimal arithmetic issues.

Let’s demonstrate money representation with golang and follow the ISO format.

Currency represents an ISO 4217 currency code

package money

// Currency represents an ISO 4217 currency code
type Currency string

// Common currency codes
const (
EGP Currency = "EGP"
SAR Currency = "SAR"
USD Currency = "USD"
EUR Currency = "EUR"
GBP Currency = "GBP"
JPY Currency = "JPY"
)

Money struct representation

// Money represents a monetary value
type Money struct {
amount uint64
currency Currency
}

// New creates a new Money instance
func New(amount uint64, currency Currency) *Money {
return &Money{
amount: amount,
currency: currency,
}
}

Operation over money struct


import (
"errors"
)

// Add adds two Money values of the same currency
func (m *Money) Add(other *Money) (*Money, error) {
if m.currency != other.currency {
return nil, errors.New("cannot add different currencies")
}
sum := m.amount + other.amount
if sum < m.amount { // Check for overflow
return nil, errors.New("addition overflow")
}
return &Money{amount: sum, currency: m.currency}, nil
}

// Subtract subtracts one Money value from another of the same currency
func (m *Money) Subtract(other *Money) (*Money, error) {
if m.currency != other.currency {
return nil, errors.New("cannot subtract different currencies")
}
if m.amount < other.amount {
return nil, errors.New("insufficient funds")
}
return &Money{amount: m.amount - other.amount, currency: m.currency}, nil
}

// Multiply multiplies a Money value by a factor
func (m *Money) Multiply(factor uint64) (*Money, error) {
result := m.amount * factor
if factor != 0 && result/factor != m.amount { // Check for overflow
return nil, errors.New("multiplication overflow")
}
return &Money{amount: result, currency: m.currency}, nil
}

// Divide divides a Money value by a divisor
func (m *Money) Divide(divisor uint64) (*Money, error) {
if divisor == 0 {
return nil, errors.New("division by zero")
}
return &Money{amount: m.amount / divisor, currency: m.currency}, nil
}

Another more interesting thing that we can demonstrate is the money conversion, we can write simple example for demostration.

package money

import (
"math/big"
)

// ConversionRate represents an exchange rate between two currencies
type ConversionRate struct {
from Currency
to Currency
rate *big.Rat
inverted bool
}

// NewConversionRate creates a new ConversionRate
func NewConversionRate(from, to Currency, rateNum, rateDenom uint64) *ConversionRate {
return &ConversionRate{
from: from,
to: to,
rate: new(big.Rat).SetFrac64(int64(rateNum), int64(rateDenom)),
}
}

Update money struct operations and append the following function

package money

import (
"errors"
"math/big"
)

func (m *Money) Convert(rate *ConversionRate) (*Money, error) {
if m.currency != rate.from && m.currency != rate.to {
return nil, errors.New("invalid conversion rate for this currency")
}

var result big.Rat
if m.currency == rate.from {
result.Mul(new(big.Rat).SetUint64(m.amount), rate.rate)
} else {
result.Quo(new(big.Rat).SetUint64(m.amount), rate.rate)
}

resultFloat, _ := result.Float64()
if resultFloat < 0 || resultFloat > float64(^uint64(0)) {
return nil, errors.New("conversion result out of range")
}

return &Money{
amount: uint64(resultFloat),
currency: rate.to,
}, nil
}

As you can see we used uint64 it is suitable for a lot of use cases however it might not be sufficient for other use cases you can use big Int from math.big library

For real application we use config files to implement ISO standards including allowed currencies and precision of each currency, usually yaml format.

References:
Calculation example:

ISO 4217

ISO 20022

this article is powered by claude.ai for reviewing and correction.

If you found this information useful, I sincerely appreciate you taking a moment to hit the clap button and follow me to stay updated on future posts. Reader feedback is invaluable for helping guide and improve my technical writing. Until next time, happy engineering!

--

--

Ahmed Ghazey
Ahmed Ghazey

Written by Ahmed Ghazey

Seasoned Senior Staff Software Engineer with a proven track record of delivering complex software solutions, managing teams, and providing technical leadership.

No responses yet