Decimal128
Decimal128
Level 2 — BSON Data Types & Document Structure The high-precision 128-bit BSON decimal data type that guarantees exact base-10 arithmetic, serving as the definitive type for currency and financial calculations.
1. Prerequisites
- Number Types (
Int32,Int64/Long,Double,Decimal128) — The parent numeric type family. - BSON Data Types (Overview) — BSON data types overview.
2. Term Category
Core Concept (High-Precision Monetary BSON Type): Decimal128 is the 128-bit IEEE 754-2008 BSON decimal format designed for exact monetary and financial calculations without floating-point rounding errors.
3. Explanation
Environment Context
- MongoDB Core (Conforms to the IEEE 754-2008 decimal floating-point standard. Stored as 16 bytes of data. Enforced in queries using
NumberDecimal()constructors).
(1) Design Motivation — "Why did we design this?"
In database design, storing money requires absolute precision.
If you use the default Double (binary floating-point) type to store a currency balance:
- Doubles store numbers as binary fractions (powers of 2).
- This works for fractions like
0.5or0.25, but cannot represent standard base-10 fractions like0.1(ten cents) or0.01(one cent) exactly. - When you add
0.10and0.20using Doubles, the computer computes:0.30000000000000004 - This micro-fraction drift is called a floating-point rounding error. Over millions of transactions, these rounding errors aggregate, causing audits to fail.
We designed Decimal128 to solve this financial math problem.
Decimal128 uses a base-10 representation.
It preserves up to 34 decimal digits of precision, ensuring that values like 0.1 are stored exactly.
(2) The Constructor String Requirement
Because JavaScript does not have a native 128-bit decimal type, you must pass the value as a string to the NumberDecimal constructor:
NumberDecimal("19.99")
If you pass it as a raw number:
NumberDecimal(19.99)
The JavaScript interpreter will translate 19.99 into a 64-bit float before handing it to MongoDB, importing the exact rounding error you are trying to avoid.
(3) Reality Metaphor
Imagine measuring expensive spices:
- Double: A generic kitchen scale that measures in grams. It works for flour, but if you weigh saffron, a few fractions of a gram are rounded off. (Float error).
- Decimal128: A High-precision Chemistry scale in a clean lab. It measures down to the exact micro-gram. It takes longer to calibrate (CPU overhead), but ensures you don't lose a single atom of gold or spice.
(4) Code Examples
1. Correct Financial Insert
Always pass the numeric value as a quoted string:
db.wallets.insertOne({
user: "Alice",
balance: NumberDecimal("100.30") // CORRECT: Preserves exact decimal
});
2. The Float Trap (Incorrect Precision)
Let's see what happens if you pass a float instead of a string:
db.wallets.insertOne({
user: "Bob",
balance: NumberDecimal(100.30) // BAD: Passes a float first!
});
db.wallets.find();
// Output:
// { "user": "Alice", "balance": NumberDecimal("100.30") }
// { "user": "Bob", "balance": NumberDecimal("100.299999999999997") } <-- Float noise!
4. Common Mistakes & Pitfalls
Mistake 1: Omitting quotation marks around the numeric value inside the NumberDecimal constructor
The mistake: Writing NumberDecimal(10.20) inside an insert query.
Why it's wrong: As shown in the Bob example above, the JavaScript runtime parses 10.20 as a Double first.
The float noise (10.199999999999997) is created in RAM, and then saved into the Decimal128 field, defeating the purpose of the data type.
Fix: Always wrap the numeric value in quotes inside the constructor: NumberDecimal("10.20") to bypass JavaScript float conversions.
Mistake 2: Using Double Floating-Point Numbers for Financial Currency Balances
The mistake: Storing financial monetary values using standard double numbers { balance: 19.99 }.
Why it's wrong: Double floating-point numbers suffer binary rounding errors (e.g. 0.1 + 0.2 = 0.30000000000000004). Use Decimal128 (NumberDecimal("19.99")) for exact financial precision.
Incorrect:
db.accounts.updateOne({ _id: id }, { $inc: { balance: 0.1 } }); // ❌ Floating point precision loss!
Fix:
db.accounts.updateOne({ _id: id }, { $inc: { balance: NumberDecimal("0.1") } });
Mistake 3: Passing Numbers Instead of Strings to NumberDecimal() Constructor
The mistake: Constructing NumberDecimal(0.1 + 0.2) passing floating-point expressions.
Why it's wrong: Passing floating-point expressions to NumberDecimal() preserves the pre-existing float rounding error! Pass string literals NumberDecimal("0.1").
Incorrect:
NumberDecimal(0.1 + 0.2); // ❌ Preserves float rounding error 0.30000000000000004!
Fix:
NumberDecimal("0.1"); // Pass exact string literal
5. Practice Exercises
Exercise 1: Precision Financial Price Calculation
Scenario:
Calculate total price including tax for a product costing NumberDecimal("19.99") at tax rate NumberDecimal("0.0825").
Requirements:
- Use
$multiplyand$addin aggregation withDecimal128values.
Answer
Implementation
db.products.aggregate([
{
$project: {
name: 1,
basePrice: "$price",
taxAmount: { $multiply: ["$price", NumberDecimal("0.0825")] },
totalWithTax: {
$add: [
"$price",
{ $multiply: ["$price", NumberDecimal("0.0825")] }
]
}
}
}
]);
Technical Explanation
Decimal128arithmetic maintains 34 decimal digits of IEEE 754-2008 precision.- Prevents binary floating-point errors (e.g.
0.1 + 0.2 = 0.30000000000000004). - Standard choice for accounting and monetary database fields.
Exercise 2: Aggregating Decimal Sums with $sum
Scenario:
Compute the sum total of all account balances in collection accounts.
Requirements:
- Aggregate
$sumoverNumberDecimalbalance fields.
Answer
Implementation
db.accounts.aggregate([
{
$group: {
_id: null,
totalAssets: { $sum: "$balance" }
}
}
]);
Technical Explanation
$sumpreservesDecimal128types during aggregation math.- Accumulates exact balances across millions of documents without rounding drift.
- Outputs a single
Decimal128result payload.
Exercise 3: Constructing Decimal128 Objects in Node.js
Scenario:
Write Node.js MongoDB driver code to insert a Decimal128 value using Decimal128.fromString().
Requirements:
- Use
Decimal128.fromString("299.95")in Node.js.
Answer
Implementation
import { MongoClient, Decimal128 } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017");
const db = client.db("store");
await db.collection("orders").insertOne({
orderId: "ORD-9912",
amount: Decimal128.fromString("299.95")
});
Technical Explanation
Decimal128.fromString(str)parses exact decimal strings into BSON binary structures.- Avoids passing JavaScript numbers (
299.95) which would cast to 64-bit IEEE double floats. - Guarantees client-to-database precision integrity.
6. Related Terms
- Number Types (
Int32,Int64/Long,Double,Decimal128) — The parent types.
7. Key Takeaways
- Decimal128 is a 128-bit BSON type designed for exact base-10 decimal math.
- Serves as the MongoDB equivalent to PostgreSQL's
NUMERICandDECIMALtypes. - Natively prevents floating-point binary rounding errors.
- Always instantiate using
NumberDecimal("value_string"). - Always wrap the number inside quotes to prevent JavaScript float parses.
- Standard Doubles are faster to calculate; use Decimal128 strictly for money.
- Ensures database monetary audits pass without calculation drift.