How Not to Lose Money Because of Float: Store Prices the Right Way
Money demands precision. Even a single misplaced cent can cause confusion — or real losses. Yet many projects still store prices using the float type, which inevitably leads to those “mystery cents” that appear out of nowhere.

Why floats quietly lose you money
Floating-point types (float, double) follow the IEEE-754 standard and store numbers in binary, not decimal. Many decimal fractions (like 0.1) cannot be represented exactly in binary, so values are approximated. That’s why you’ll see surprises like:
echo 0.1 + 0.2; // 0.30000000000000004
This isn’t a PHP bug — it’s how floats work in almost every language (PHP, JS, Python, Java, C#, Go). When these tiny errors stack up across discounts, taxes, and invoice sums, your books start to drift by “mystery cents”.
The solution: exact numeric types
Stop storing money in binary floating types. Switch to exact decimal or integer representations that don’t accumulate rounding noise.
MySQL
- DECIMAL(p, s) — fixed-point decimal where digits are stored exactly; great for accounting and reports (e.g.,
DECIMAL(10,2)). - BIGINT — store amounts in minor units (cents, pence).
€12.99becomes1299. Fast, compact, precise. - FLOAT/DOUBLE — avoid for prices; they’re approximate by design.
PostgreSQL
- NUMERIC(precision, scale) — exact decimal (PostgreSQL’s DECIMAL); flexible and precise.
- BIGINT — same minor-unit approach as MySQL, ideal for transactional data.
- MONEY — locale-dependent and inflexible; not recommended for multi-currency systems.
Why BIGINT is often the best choice
BIGINT is a large integer type with a huge range (up to 9.22×1018). If you store prices in cents, that’s up to ~€92 trillion with cent precision — more than enough for any e-commerce or SaaS platform.
- Exact by construction: integers don’t carry rounding noise. What you store is what you get — even after millions of operations.
- Performance: integer arithmetic, comparisons, indexing, sorting, and aggregation are cheaper than
DECIMALon large datasets. - Portability: integers behave consistently across MySQL, PostgreSQL, SQLite, and ORMs. No worries about precision/scale mismatches.
- Predictable math: apply VAT/discounts, then round once to minor units; no
0.00000001artifacts. - API alignment: major payment providers (Stripe, Adyen, PayPal) use minor units in their APIs — matching them avoids cross-system rounding issues.
Exact arithmetic in PHP (avoid float at runtime too)
Even if the DB is correct, your code can reintroduce float errors. Use BCMath for decimal-safe operations:
$total = bcadd('0.1', '0.2', 2); // "0.30"
$vat = bcmul('12.99', '0.20', 2); // "2.60"
$gross = bcadd('12.99', '2.60', 2); // "15.59"
bcadd, bcmul, bcdiv work on strings with a fixed scale — so “0.1 + 0.2” always equals “0.3”.
Laravel example: tiny cast, big safety
Store cents in the DB, expose “12.99” in PHP. A minimal cast does the conversion both ways:
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AsPrice implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (empty($value)) {
return 0;
}
return bcdiv((string) $value, '100', 2);
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
if (empty($value)) {
return 0;
}
return bcmul((string) $value, '100', 0);
}
}
Attach the cast on your model:
class Product extends \Illuminate\Database\Eloquent\Model
{
protected $casts = [
'price' => \App\Casts\AsPrice::class,
];
}
Schema with strict currencies
Even if you start with one currency, add a currency column now. Keep it strict with ENUM for EUR / USD / GBP:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->bigInteger('price'); // stored in minor units (cents)
$table->enum('currency', ['EUR', 'USD', 'GBP'])->default('EUR');
$table->timestamps();
});
Key takeaways
- Floats are approximate (IEEE-754), and that’s universal across languages — don’t use them for money.
- Prefer BIGINT (minor units) for speed, safety, and portability; use DECIMAL/NUMERIC where reporting demands decimals.
- Do arithmetic with BCMath in PHP to keep results exact.
- A tiny Laravel cast makes prices ergonomic in code while your database stays precise.
- Store currency as ENUM (
EUR,USD,GBP) to prevent bad data.
Get in touch
Have a project
in mind?
Tell us the context and the outcome you want. We’ll reply within 1 business day with the simplest next step (timeline, rough budget, or quick audit).