A simplified banking backend built to practice backend correctness and concurrency-safe database operations. The system uses a ledger (ledger_transactions) as an append-only event log and an accounts table for current balances. A FastAPI layer exposes the functionality via REST endpoints with Swagger UI.
Most “banking API” demos are CRUD-only. This project focuses on:
- Atomic operations (all-or-nothing updates)
- Concurrency safety (row-level locking to prevent race conditions)
- Auditability (append-only ledger of all events)
- Deterministic history (
ORDER BY timestamp, transaction_id)
- Python
- FastAPI (OpenAPI/Swagger UI)
- PostgreSQL
- psycopg (v3)
- No account merges
- No account “active/inactive” status
account_idis unique forever (cannot be reused)- No authentication/authorization (out of scope for this iteration)
Client (Swagger/curl)
↓ HTTP/JSON
FastAPI (main.py)
↓ method calls
BankingSystemImpl (domain adapter)
↓ delegates
BankingStore (psycopg + SQL transactions/locks)
↓ SQL
PostgreSQL (accounts + ledger_transactions)
accounts: current balance per accountledger_transactions: append-only log of operations (deposits, transfers, payments, cashbacks)payment_seq: sequence to generatepaymentNids
Create a file named schema.sql in the project root:
BEGIN;
DROP TABLE IF EXISTS ledger_transactions CASCADE;
DROP TABLE IF EXISTS accounts CASCADE;
DROP SEQUENCE IF EXISTS payment_seq;
CREATE TABLE accounts (
account_id TEXT PRIMARY KEY,
created_at BIGINT NOT NULL,
balance BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE ledger_transactions (
transaction_id BIGSERIAL PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(account_id) ON DELETE RESTRICT,
timestamp BIGINT NOT NULL,
operation TEXT NOT NULL,
amount BIGINT NOT NULL,
payment_ref TEXT NULL,
deposited BOOLEAN NULL
);
CREATE SEQUENCE payment_seq START 1;
CREATE INDEX idx_ledger_account_time
ON ledger_transactions(account_id, timestamp);
CREATE INDEX idx_cashback_due
ON ledger_transactions(timestamp)
WHERE operation = 'cashback' AND deposited = FALSE;
COMMIT;
From WSL:
sudo -u postgres psql <<'SQL'
DROP DATABASE IF EXISTS banking_db;
DROP ROLE IF EXISTS bank_user;
CREATE ROLE bank_user WITH LOGIN PASSWORD 'bank_pass';
CREATE DATABASE banking_db OWNER bank_user;
GRANT ALL PRIVILEGES ON DATABASE banking_db TO bank_user;
SQLmake installpsql "postgresql://bank_user:bank_pass@127.0.0.1:5432/banking_db" -f schema.sqlmake devSwagger UI:
For local simplicity, main.py corrently hard-codes the DSN:
DATABASE_URL = "postgresql://bank_user:bank_pass@127.0.0.1:5432/banking_db"The next iteration of implamentation id to move to cloud. This willbe replased with an envirnment variable.
GET / health
POST / accounts-- create accountGET / accounts-- list account IDsGET / {account_id/balance}-- currenct balance for a specified account
POST / deposits-- add money to accountPOST / transfers-- transfer money between accountsPOST / payments-- make payments that apply cashback deposits
GET /accounts/{account_id}/transactions?timestamp=<ms>-- all ledger entires for a specified account