TT Lab
Get started
Learn Learning paths Courses

Idempotency — Two Clicks, One Charge

Preserve balances when a transfer fails halfway

Continue in TT Lab

Goal

You tie the debit, the credit, and the transfer record together atomically, and reproduce a concurrent balance race.

Why it matters

The process died after the money was taken from the sending account and before it was added to the receiving account. The retry had no transfer id, so it took the money out again. Conversely, if only the transfer record is committed first, the retry judges it complete and the deposit is lost forever. Even if you compute amounts exactly with integers, the result is wrong if the transaction boundary is wrong.

Steps

  1. In /root/work/idem-ledger-transfer-lab/service.py, init_db(path) idempotently creates accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL) and transfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL).

Prepare it once at the beginning. It does not overwrite an existing file.

mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab
  1. In /root/work/idem-ledger-transfer-lab/service.py, add_account(path, account_id, amount) allows only an int of 0 or more (not bool) and creates the account with INSERT. An account that already exists is rejected with sqlite3.IntegrityError, and the balance is preserved.

  2. In /root/work/idem-ledger-transfer-lab/service.py, balance(path, account_id) returns the account balance, and raises KeyError if it does not exist.

  3. In /root/work/idem-ledger-transfer-lab/service.py, validate_transfer(source,target,amount) returns None if source!=target and amount is a positive int (not bool), and otherwise raises ValueError.

  4. In /root/work/idem-ledger-transfer-lab/service.py, transfer(path,tx_id,source,target,amount,fault=lambda:None) returns False for the same id and content, and raises ValueError for an id conflict, a missing account, or an insufficient balance. A new transfer atomically runs debit → fault() → credit → insert into transfers, and returns True.

  5. In /root/work/idem-ledger-transfer-lab/service.py, history(path) returns transfers as a list of (id,source,target,amount) tuples in ascending id order.

  6. In /root/work/idem-ledger-transfer-lab/service.py, total_balance(path) is the sum of accounts.balance. If there are no accounts, it is 0.

  7. In /root/work/idem-ledger-transfer-lab/service.py, compete(path,source,target,amount) runs transfer with the same amount in two threads using different ids race-a and race-b. It returns the list of results where only ValueError is converted to False. If the balance covers only one transfer, there is exactly one success.

Notes

Create the ledger tables

In /root/work/idem-ledger-transfer-lab/service.py, init_db(path) idempotently creates accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL) and transfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL).

Prepare it once at the beginning. It does not overwrite an existing file.

mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab

Put the uniqueness in the DB so that the same transfer id is not committed twice.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/01-contract.sh.

Create an account only once

In /root/work/idem-ledger-transfer-lab/service.py, add_account(path, account_id, amount) allows only an int of 0 or more (not bool) and creates the account with INSERT. An account that already exists is rejected with sqlite3.IntegrityError, and the balance is preserved.

Keep an initialization UPSERT from resetting the real balance to the initial value.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/02-contract.sh.

Tell a missing account from 0 won

In /root/work/idem-ledger-transfer-lab/service.py, balance(path, account_id) returns the account balance, and raises KeyError if it does not exist.

If you turn "missing" into 0, a transfer can proceed to a wrong account.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/03-contract.sh.

Validate the transfer input

In /root/work/idem-ledger-transfer-lab/service.py, validate_transfer(source,target,amount) returns None if source!=target and amount is a positive int (not bool), and otherwise raises ValueError.

Reject a transfer to the same account and a negative transfer beforehand.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/04-contract.sh.

Tie three writes into one transaction

In /root/work/idem-ledger-transfer-lab/service.py, transfer(path,tx_id,source,target,amount,fault=lambda:None) returns False for the same id and content, and raises ValueError for an id conflict, a missing account, or an insufficient balance. A new transfer atomically runs debit → fault() → credit → insert into transfers, and returns True.

Raise an exception on purpose after the debit and check that both the balances and the transfer record are back to their original state.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/05-contract.sh.

Read the transfer history deterministically

In /root/work/idem-ledger-transfer-lab/service.py, history(path) returns transfers as a list of (id,source,target,amount) tuples in ascending id order.

State ORDER BY explicitly so that you do not confuse input order with lookup order.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/06-contract.sh.

Check the conserved amount

In /root/work/idem-ledger-transfer-lab/service.py, total_balance(path) is the sum of accounts.balance. If there are no accounts, it is 0.

Verify separately the return value of each request and the conservation of the total amount.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/07-contract.sh.

Concurrent transfers do not exceed the balance

In /root/work/idem-ledger-transfer-lab/service.py, compete(path,source,target,amount) runs transfer with the same amount in two threads using different ids race-a and race-b. It returns the list of results where only ValueError is converted to False. If the balance covers only one transfer, there is exactly one success.

If you check the balance beforehand outside the transaction, two requests can see the same balance and both succeed.

After saving, check with bash /opt/lab/checks/idem-ledger-transfer-lab/08-contract.sh.