A finance officer at a mid sized bank was closing out the month when the numbers refused to line up. Customer accounts held a few cents more, in total, than the bank's own settlement records said they should. She ran the reconciliation again, certain it was her mistake. Same result. She pulled in an engineer, who spent two days going line by line through the transaction logic looking for the missing bug. There was no missing transaction. There was no attacker. There was no single line of broken code anyone could point to. The gap had been forming quietly for months, a fraction of a cent at a time, every single time interest was calculated or a fee was split between accounts.
Nobody had done anything wrong. The mistake had been made much earlier, before either of them had joined the company, in a decision about how money would be represented inside the system in the first place.
The first question I would ask
Before I would agree to use NestJS to build or touch any part of a bank's money handling, the first question I would ask is simple. How are amounts stored and calculated internally, as ordinary numbers, or as something built specifically to represent money exactly.
It sounds like a small detail. It is the single decision that determines whether the numbers can ever quietly drift apart from reality.
Why plain numbers cannot be trusted with money
JavaScript, like most languages, represents regular numbers using floating point. Floating point is excellent for scientific calculations and graphics, where being off by a tiny fraction almost never matters. It is not designed to represent decimal currency amounts exactly. A value like zero point one, which looks perfectly ordinary to a person, cannot always be stored exactly in that format. Individually, the error is too small to notice. Add, subtract, split, and multiply that number millions of times across a real banking system, and those tiny errors accumulate into amounts that are large enough to matter, and impossible to trace back to a single obvious bug.
This is exactly why the reconciliation report in the opening story slowly drifted. Nothing was ever technically wrong with the logic. The numbers themselves were never being represented with the precision the business rules assumed they had.
How NestJS keeps money precise
This is where the actual engineering discipline comes in, and it has to be enforced at every layer, not just trusted in one place.
The first layer is validation at the edge of the system, the DTO. Before an amount is even accepted, it should be validated as a properly formatted decimal string, not a loose number.
import { IsString, Matches } from 'class-validator';
export class CreateTransactionDto {
@IsString()
@Matches(/^\d+(\.\d{1,4})?$/, {
message: 'amount must be a valid decimal string with up to four decimal places',
})
amount: string;
}
Accepting the amount as a string, rather than a number, means it arrives exactly as written, with no floating point conversion happening before validation even runs.
The second layer is the database schema itself. The column that stores the amount should use a fixed precision decimal type, not a floating point column, so the database itself refuses to silently round or approximate the value.
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('ledger_entries')
export class LedgerEntry {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'numeric', precision: 15, scale: 4 })
amount: string;
}
The third layer is arithmetic itself. Any place in a NestJS service where amounts are added, subtracted, or compared should never rely on plain JavaScript math on those values directly. Instead, the calculation should happen through a library built for exact decimal arithmetic, so precision is preserved through every step, not only at storage.
import Decimal from 'decimal.js';
function calculateTotal(amounts: string[]): string {
return amounts
.reduce((total, current) => total.plus(new Decimal(current)), new Decimal(0))
.toFixed(4);
}
Put together, these three layers mean an amount is validated as exact the moment it enters the system, stored as exact the moment it is saved, and calculated as exact every time it is touched. There is no point in the chain where a silent rounding error can slip in.
The bigger picture
NestJS will not automatically protect a bank from this mistake. Nothing in the framework forces a developer to use decimal strings instead of plain numbers, or a fixed precision column instead of a floating point one. What NestJS gives you is a clean, enforceable place to put each of these decisions, a validation rule at the DTO layer, a column definition at the entity layer, and a consistent calculation approach at the service layer, so the whole team follows the same discipline instead of everyone reinventing it, or forgetting it, feature by feature.
If your team is dealing with numbers that quietly do not add up, or you are building something from scratch and want the precision handled correctly from day one, I would be glad to talk through how to structure it.
I am Peace Melodi, a backend software engineer. If you want your business to scale big, comfortably handling millions of users without breaking, with strong scalability and security in place, feel free to reach out.
LinkedIn: https://www.linkedin.com/in/melodi-peace-406494368
GitHub: https://github.com/PeaceMelodi
Top comments (0)