DEV Community

Cover image for I once watched a fintech app lose money to 0.1 plus 0.2, here is how NestJS stops that nightmare
Peace Melodi
Peace Melodi

Posted on

I once watched a fintech app lose money to 0.1 plus 0.2, here is how NestJS stops that nightmare

Open any JavaScript console right now and type 0.1 plus 0.2. It will not give you 0.3. It gives you 0.30000000000000004. This is not a bug in JavaScript specifically, almost every programming language does this, because computers store decimal numbers in a way that cannot perfectly represent numbers like 0.1 in binary. Most of the time nobody notices, a pixel is off by a hair, a chart rounds slightly wrong.

In a fintech backend, this exact quirk can quietly cost real money, and the scary part is that it usually works perfectly fine for months before it shows up.

How this actually shows up in a banking backend

Picture a simple fee calculation. A transfer of ten dollars with a small percentage fee attached.

@Injectable()
export class FeeService {
  calculateFee(amount: number): number {
    return amount * 0.029 + 0.30;
  }
}
Enter fullscreen mode Exit fullscreen mode

This looks completely harmless. Run it a few times by hand and the numbers look right. Run it across millions of real transactions with all kinds of decimal amounts, and small rounding errors start creeping in, a fraction of a cent here, a fraction of a cent there. Nobody notices for a while, because a fraction of a cent is invisible on a receipt. Then one day the finance team tries to reconcile total fees collected against total fees calculated, and the numbers do not match, and nobody can figure out why, because every individual transaction looks correct in isolation.

Why the normal number type is the wrong tool for money

The core issue is that JavaScript's regular number type was never built to represent money precisely, it was built for general math and graphics, where a tiny rounding error genuinely does not matter. Money is different. Money needs to be exact, always, every single time, with zero tolerance for a rounding error that quietly grows across millions of transactions.

The fix that real financial systems use is surprisingly simple, stop representing money as a decimal number at all. Store every amount as a whole number of the smallest currency unit, cents for dollars, instead of dollars themselves.

@Entity()
export class Transaction {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'bigint' })
  amountInCents: number;

  @Column()
  currency: string;
}
Enter fullscreen mode Exit fullscreen mode

Ten dollars is no longer 10, it is 1000. There is no decimal point left anywhere for a rounding error to hide in, because whole numbers do not have this floating point problem the same way decimals do.

Enforcing this at the door with a validation pipe

Storing money correctly only helps if nothing sneaks a raw decimal dollar amount past the entry point. A custom NestJS pipe can catch this the moment a request comes in, before it ever reaches your business logic.

@Injectable()
export class MoneyValidationPipe implements PipeTransform {
  transform(value: any) {
    if (!Number.isInteger(value)) {
      throw new BadRequestException(
        'Amount must be provided as a whole number of cents, not a decimal',
      );
    }
    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode
@Controller('transfers')
export class TransfersController {
  @Post()
  async createTransfer(
    @Body('amountInCents', MoneyValidationPipe) amountInCents: number,
  ) {
    return this.transfersService.process(amountInCents);
  }
}
Enter fullscreen mode Exit fullscreen mode

Anyone, or anything, that tries to send a raw decimal dollar amount into this endpoint gets rejected immediately, with a clear reason, instead of quietly being accepted and slowly corrupting your numbers over time.

Doing the fee math safely, in cents

With everything living in whole cents, calculations like fees need to round deliberately at each step, rather than trusting decimal math to behave.

@Injectable()
export class FeeService {
  calculateFeeInCents(amountInCents: number): number {
    const percentageFee = Math.round(amountInCents * 0.029);
    const fixedFeeInCents = 30;

    return percentageFee + fixedFeeInCents;
  }
}
Enter fullscreen mode Exit fullscreen mode

Rounding explicitly at the exact moment the percentage is calculated means every transaction lands on a whole cent, on purpose, instead of hoping the decimal math happens to work out.

The bigger picture

None of this is really about JavaScript being flawed, every language with standard decimal numbers has this same quirk, and every serious financial system, in any language, works around it the same basic way, treat money as whole units, never as a raw decimal. NestJS makes this genuinely easy to enforce consistently, a shared entity type, a validation pipe guarding the entry point, and services that round deliberately rather than accidentally.

The unsettling part of this bug is how quiet it is. It never throws an error. It never crashes anything. It just slowly, invisibly disagrees with reality, one fraction of a cent at a time, until someone finally goes looking for the missing money and cannot find where it went.

If you are building anything that touches real money and want to make sure this exact quiet failure never has a chance to start, that is exactly the kind of foundation worth getting right early.

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)