# Money is not a double: A Hard Lesson from Building a Trading App

When you build your first fintech app in Flutter, using `double` for money feels completely normal.

```javascript
double price = 10.25;
```

It compiles.  
It runs.  
The UI looks correct.  
QA passes it.

So what's the problem?

The problem is that `double` was never meant to store money.

And one day, usually when your product grows, that decision quietly comes back to hurt you.

* * *

## The Problem Nobody Sees at First

In Dart (like most languages), `double` uses floating-point representation.

Here's something simple:

```javascript
void main() {
  double a = 0.1;
  double b = 0.2;
  print(a + b);
}
```

Output:

```javascript
0.30000000000000004
```

That extra `4` at the end?

That's not a bug in Dart.  
That's how floating-point numbers work.

Most decimal numbers cannot be stored exactly in binary.

And `double` stores numbers in binary.

* * *

## Why This Doesn’t Break Your App Immediately

Most traditional fiat currencies use only 2 decimal places:

*   10.25
    
*   99.99
    
*   1.50
    

Even if the internal number is slightly wrong, we usually format it like this:

```javascript
value.toStringAsFixed(2)
```

And the UI shows:

```plaintext
10.25
```

So everything *looks* correct.

The error is still there.  
It's just hidden.

* * *

## When Things Start Breaking: Crypto

Now imagine your app starts supporting:

*   Bitcoin - 8 decimal places
    
*   Ethereum - 18 decimal places
    
*   Tether - 6 decimal places
    

Now precision really matters.

Example:

```plaintext
0.00000013 BTC
```

That last digit is real money.

If you use `double`:

*   Adding values can shift decimals.
    
*   Multiplication introduces rounding.
    
*   Conversions between currencies create tiny mismatches.
    
*   Formatting hides precision loss.
    
*   Parsing user input mutates values silently.
    

And the worst part, its a bug hidden in plain sight but still goes away unnoticed.

* * *

## Issue entrypoint: UI & Backend Round Trip

This is where things actually break.

Imagine this flow:

1.  Backend sends: `"0.00000013"`
    
2.  You convert it to `double`
    
3.  User edits the amount
    
4.  You format and send it back
    

Somewhere in that round trip, the value changes slightly.

You expected:

```plaintext
0.00000013
```

You send:

```plaintext
0.00000012
```

In financial systems, that is unacceptable.

Small rounding issues lead to:

*   Ledger mismatches
    
*   Transaction validation failures
    
*   “Amount does not match” backend errors
    

Floating point errors are small.

But money systems require exactness, not approximation.

* * *

## Why double Is the Wrong Tool

Floating point numbers are stored like this:

```markdown
sign × mantissa × 2^exponent
```

They are optimised for:

*   Scientific calculations
    
*   Graphics
    
*   Physics simulations
    

They are not optimised for:

*   Accounting
    
*   Trading systems
    
*   Ledger calculations
    
*   User-entered currency
    

Money is decimal.  
Floating point is binary.

That mismatch is the root cause.

* * *

# So what's the right way?

## 1\. Store the Smallest Unit

Instead of:

```javascript
double amount = 10.25;
```

Do this:

```javascript
int amountInCents = 1025;
```

For crypto:

*   1 BTC = 100,000,000 satoshis
    
*   Store satoshis as `int`
    

No rounding.  
No precision loss.  
No floating point.

* * *

## 2\. Create a Proper Money Type

Instead of passing raw numbers everywhere, define a safe structure.

```javascript
class Money {
  final BigInt amount;   // smallest unit
  final int scale;       // decimal places
  final String currency;

  Money({
    required this.amount,
    required this.scale,
    required this.currency,
  });

  String format() {
    final divisor = BigInt.from(10).pow(scale);
    final whole = amount ~/ divisor;
    final fraction = (amount % divisor)
        .toString()
        .padLeft(scale, '0');
    return "$whole.$fraction $currency";
  }
}
```

Now:

*   Precision is controlled.
    
*   Each currency defines its scale.
    
*   UI formatting is deterministic.
    
*   Math operations stay exact.
    

This is how robust financial systems are built.

* * *

# Example: Showing Safe Values on a Transaction History Screen

Let's say your backend sends smallest unit values.

Transaction model:

```javascript
class TransactionModel {
  final String title;
  final BigInt amount; // smallest unit
  final int scale;
  final String currency;

  TransactionModel({
    required this.title,
    required this.amount,
    required this.scale,
    required this.currency,
  });

  String formattedAmount() {
    final divisor = BigInt.from(10).pow(scale);
    final whole = amount ~/ divisor;
    final fraction = (amount % divisor)
        .toString()
        .padLeft(scale, '0');

    return "$whole.$fraction $currency";
  }
}
```

UI:

```javascript
ListView.builder(
  itemCount: transactions.length,
  itemBuilder: (context, index) {
    final txn = transactions[index];

    return ListTile(
      title: Text(txn.title),
      trailing: Text(
        txn.formattedAmount(),
        style: const TextStyle(
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  },
);
```

Notice what's missing?

No `double`.  
No `toStringAsFixed`.  
No floating point math.

Just exact numbers.

* * *

## One Rule You Should Never Break

Never do this for currency:

```typescript
double.parse(userInput)
```

Instead:

*   Keep input as string
    
*   Validate format
    
*   Convert manually to smallest unit
    
*   Store as `BigInt` or `int`
    

Money should never pass through floating point.

* * *

# The Lesson

Using `double` for money is easy.

It works in prototypes.  
It works in demos.  
It works when you only support 2 decimal currencies.

But products evolve.

They add:

*   Crypto
    
*   Micro-transactions
    
*   Fractional trading
    
*   High precision calculations
    

And that's when floating point becomes a silent production bug.

It doesn't crash your app.

It just slowly corrupts your numbers.

* * *

## Final Thought

Floating point errors are invisible in the beginning.

But in financial systems, invisible errors become very visible money.

And systems involving money don't forgive approximation.
