Idempotency — Two Clicks, One Charge
Preserve balances when a transfer fails halfway
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
- 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
-
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. -
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. -
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. -
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. -
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. -
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. -
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
- Do it in the existing lab-dev environment, without the internet or package installation.
- Each step runs within a 45-second grading budget. Do not add real sleep or network calls.
- Grading freshly imports the submitted module and checks it with independent inputs and a temporary DB. Implement the contract instead of returning expected values as constants.
- FastAPI official documentation · pytest official documentation · Python sqlite3
- Limitation: this is not a system that replaces the accounting, audit, and legal requirements of a real financial ledger. It does not handle currencies, decimal units, fees, or multiple currencies, and amounts are positive integers in the smallest unit. A call to an external payment system is not included in this DB transaction, so it needs a separate design.
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.