It was just past two in the morning when the message came in. A founder I had spoken with a few times sent a single line. The numbers do not add up. The dashboard was showing that the platform owed its users more money combined than the actual balance sitting in the company bank account. Nobody had broken in. No card had been stolen. There was no fraud alert anywhere in the logs. What had actually happened was quieter and much harder to notice at first. A wave of transfers had hit the system during a busy period, one request had been retried after a slow network response, and somewhere in that retry an account balance had been increased twice while only being decreased once. A small gap, multiplied across thousands of transactions, had turned into a real financial hole.
That kind of incident does not usually come from a hack. It comes from how the backend was built to track money in the first place.
The fatal flaw of single balance updates
Most applications that handle money start out storing a single number for each account and updating it directly. A transfer looks simple on the surface. Subtract from one account, add to another, two separate database writes. It works fine in a demo. The problem shows up the moment something interrupts that sequence. A server restarts halfway through. A network call times out and gets retried. A bug causes one write to run twice while the other only runs once. None of these are rare edge cases in a real production system, they happen constantly at scale.
Once that happens, the database is left believing money exists that was never actually created anywhere, or believing money vanished that was never actually spent. There is nothing in a single number balance that can catch this. The number simply is whatever the last write left it as.
The rule that catches this before it happens
The fix used by every serious financial system is not new. It is double entry accounting, and it has been used by accountants for centuries for exactly this reason. Every movement of value has two sides. One account decreases, one account increases, and those two amounts must always be equal. Money is never created out of nothing and never quietly disappears. It only moves from one place to another.
The value of this rule is that it is self checking. If the two sides of a transaction do not match, something is wrong, and you know it immediately, before the transaction is ever allowed to complete. A single balance column cannot tell you that. A ledger built on debit and credit legs can.
Enforcing this rule in NestJS
This is where the framework earns its place. NestJS will not know anything about accounting on its own, but it gives you a clean, structured place to enforce the rule every single time, instead of hoping every developer who touches the transfer code remembers to check it themselves.
The first piece is validating that every transaction balances before anything is saved. A transaction is really a list of legs, each one marked as a debit or a credit with an amount, and the total of all debits must equal the total of all credits.
private validateAccountingParity(
entries: { type: EntryType; amount: string }[],
): void {
let totalDebit = 0;
let totalCredit = 0;
for (const entry of entries) {
const numericAmount = parseFloat(entry.amount);
if (entry.type === EntryType.DEBIT) {
totalDebit += numericAmount;
} else if (entry.type === EntryType.CREDIT) {
totalCredit += numericAmount;
}
}
if (Math.abs(totalDebit - totalCredit) > 0.0001) {
throw new BadRequestException(
`Accounting Parity Violation: Total Debits (${totalDebit}) must equal Total Credits (${totalCredit}).`,
);
}
}
If the numbers do not line up, the transaction is rejected outright. Nothing touches the database.
The second piece is making sure the transfer is atomic. A transfer usually touches more than one ledger entry, and if only some of those entries get saved before something fails, the ledger is left in a broken state. Wrapping the whole operation in a single database transaction means it either completes fully or rolls back completely, with nothing left half done.
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// create and save each ledger entry here
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
The third piece deals with concurrency. If two transfers touch the same account at almost the same moment, both could read the same starting balance and make decisions based on stale information. Locking the account rows involved while a transfer is in progress prevents that.
const account = await queryRunner.manager.findOne(Account, {
where: { id: leg.accountId },
lock: { mode: 'pessimistic_write' },
});
To avoid two transfers locking accounts in a different order and deadlocking each other, the legs of a transaction should be sorted consistently, for example by account id, before any locks are acquired.
See it in action
I built all of this into a small project called Parity Ledger, a double entry ledger engine with an interactive dashboard so you can create accounts, run transfers, and watch the parity check and balances update in real time.
The bigger picture
None of this makes a backend immune to every kind of mistake. NestJS does not understand accounting rules by itself, and no framework will stop a developer from writing a feature that quietly bypasses the ledger entirely. What structure like this actually gives you is a single, enforced place where the rule lives. Every transfer, no matter who wrote the code or when, has to pass through the same validation, the same atomic transaction, and the same locking behavior. That consistency is what keeps a two in the morning message like the one at the start of this article from ever needing to be sent.
If you are building something where money, or anything else that must always balance, moves between parts of your system, I would be glad to talk through how this kind of structure could apply to what you are working on.
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)