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}