Careeroza
Real World Example
Wallet Transfers with Row Locks
Wallet apps (Paytm, PhonePe, Amazon Pay) must never let two simultaneous transfers spend the same balance. A transaction with pessimistic row locks (SELECT ... FOR UPDATE) makes the second transfer wait.
src/wallet/wallet.service.ts
import { BadRequestException, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Wallet } from './wallet.entity';
import { LedgerEntry } from './ledger-entry.entity';
@Injectable()
export class WalletService {
constructor(private readonly dataSource: DataSource) {}
transfer(fromUserId: number, toUserId: number, amountPaise: number) {
return this.dataSource.transaction('SERIALIZABLE', async (m) => {
// lock both rows in a fixed order to avoid deadlocks
const [a, b] = [fromUserId, toUserId].sort((x, y) => x - y);
const wallets = await m.find(Wallet, {
where: [{ userId: a }, { userId: b }],
lock: { mode: 'pessimistic_write' },
});
const from = wallets.find((w) => w.userId === fromUserId)!;
const to = wallets.find((w) => w.userId === toUserId)!;
if (from.balancePaise < amountPaise) throw new BadRequestException('Insufficient balance');
from.balancePaise -= amountPaise;
to.balancePaise += amountPaise;
await m.save([from, to]);
await m.insert(LedgerEntry, [
{ walletId: from.id, amountPaise: -amountPaise, type: 'TRANSFER_OUT' },
{ walletId: to.id, amountPaise, type: 'TRANSFER_IN' },
]);
return { from: from.balancePaise, to: to.balancePaise };
});
}
}Example
curl -X POST http://localhost:3000/wallet/transfer -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"toUserId":42,"amountPaise":50000}'Example output
{"from":125000,"to":80000}