Every movement is recorded as at least two entries (debit and credit) that sum to zero.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # ====================================================================== # Q: What is a double-entry ledger and why do fintech companies use it? # # Every movement is recorded as at least two entries (debit and credit) that sum # to zero. Entries are append-only: we never UPDATE a balance, we add a # reversing entry instead. The balance is the sum of entries (or a cached # balance updated in the same transaction). It gives a full audit trail and easy # reconciliation. # ====================================================================== def transfer(src_id, dst_id, amount: Decimal, ref: str): with transaction.atomic(): # lock in a fixed order to avoid deadlock accts = {a.pk: a for a in Account.objects.select_for_update() .filter(pk__in=[src_id, dst_id]).order_by('pk')} if accts[src_id].balance < amount: raise InsufficientFunds tx = LedgerTransaction.objects.create(reference=ref) # unique ref = idempotency Entry.objects.bulk_create([ Entry(tx=tx, account_id=src_id, amount=-amount), Entry(tx=tx, account_id=dst_id, amount=amount)]) Account.objects.filter(pk=src_id).update(balance=F('balance') - amount) Account.objects.filter(pk=dst_id).update(balance=F('balance') + amount) |