Hook: Interviewers love this question because a bank transfer is the cleanest way to test whether you truly understand the phrase all or nothing.
Question: In a banking transaction scenario, how do you move money from one account to another safely in SQL?
Answer: Put the debit and credit inside one explicit transaction. First subtract from the source account, then add to the destination account, and commit only when both steps succeed. If anything fails, roll back so the database never shows money leaving one account without arriving in the other.
Interview-Ready Answer: I would wrap the transfer in a single SQL transaction so it is atomic, which means either the whole transfer happens or none of it does. I would also guard against overdrafts by checking the balance before debiting, or by enforcing a CHECK constraint so the database protects the rule for me. That way I avoid half-finished transfers, inconsistent balances, and race conditions when two sessions try to update the same account at once.
A bank transfer is a classic transaction, which is a group of SQL changes treated as one unit. The key idea is ACID: Atomicity means all or nothing, Consistency means rules stay valid, Isolation means other sessions do not see a half-done transfer, and Durability means a committed transfer survives a crash.
SELECT ... FOR UPDATE to lock before editing.WAL or redo log. The log is the safety net that makes recovery possible after a crash.COMMIT makes the transfer durable. If the app crashes before commit, the database can roll back the incomplete work from the log.| Mode | What happens | Bank transfer risk |
|---|---|---|
| Autocommit | Each statement is its own transaction | Very risky for multi-step transfers |
| Explicit transaction | Many statements are grouped together | Safe for debit + credit |
Use a transaction any time several changes must stay in sync: money movement, seat booking, inventory checkout, loyalty points, or ledger posting. In banking, the rule is simple: if one side fails, the whole story fails.
With an index on account_id, each row lookup is usually around O(log N). The real cost is not the math; it is the lock and the log flush. For OLTP systems, keep transactions short, often in the low milliseconds to tens of milliseconds, because long transactions increase waiting and deadlock risk. Also, never store money in floating-point types such as FLOAT; use NUMERIC or DECIMAL so you do not get rounding bugs like 0.3000000004.
READ COMMITTED is common, but stronger levels like SERIALIZABLE reduce anomalies at the cost of more retries.Memory idea: think of a transfer like moving a sealed envelope from one safe to another. You do not announce it done until the envelope is in the second safe and the door is locked.
Imagine a mobile banking app with a core ledger service. A customer sends $200 from checking to savings. The service first debits checking, then credits savings, and both actions must be part of the same SQL transaction.
What goes wrong when developers misunderstand this? Suppose the debit is committed first, then the app times out before the credit happens. The user sees missing money, support sees angry tickets, and the reconciliation job later reports that the ledger no longer balances. Logs may show one successful UPDATE followed by a timeout, restart, or deadlock error. In production, that kind of bug creates both financial risk and trust damage, which is why interviewers care so much about transaction handling.
-- PostgreSQL-style demo of a safe bank transfer.
-- It uses NUMERIC for money, a balance check, and one transaction per transfer.
-- If the debit cannot happen, the credit also does not happen.
DROP TABLE IF EXISTS accounts;
CREATE TEMP TABLE accounts (
account_id INTEGER PRIMARY KEY,
owner TEXT NOT NULL,
balance NUMERIC(12,2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts (account_id, owner, balance) VALUES
(1, 'Ava', 500.00),
(2, 'Ben', 300.00);
-- Successful transfer: move 200 from Ava to Ben.
-- The debit and credit are tied together inside one transaction.
BEGIN;
WITH deducted AS (
UPDATE accounts
SET balance = balance - 200.00
WHERE account_id = 1
AND balance >= 200.00
RETURNING account_id
)
UPDATE accounts
SET balance = balance + 200.00
WHERE account_id = 2
AND EXISTS (SELECT 1 FROM deducted);
COMMIT;
SELECT account_id, owner, balance
FROM accounts
ORDER BY account_id;
-- Edge case: overdraft attempt.
-- Ava only has 300 left, so this transfer should not happen.
-- The WHERE clause prevents a negative balance.
BEGIN;
WITH deducted AS (
UPDATE accounts
SET balance = balance - 1000.00
WHERE account_id = 1
AND balance >= 1000.00
RETURNING account_id
)
UPDATE accounts
SET balance = balance + 1000.00
WHERE account_id = 2
AND EXISTS (SELECT 1 FROM deducted);
COMMIT;
SELECT account_id, owner, balance
FROM accounts
ORDER BY account_id;
-- If you removed the balance check above, the CHECK constraint would reject
-- the negative balance and the whole transaction would fail instead of leaving
-- the database in a broken half-transfer state.Follow-up & Tricky Questions:
SELECT ... FOR UPDATE? It locks the rows you plan to change, which prevents another session from reading and updating them at the same time and causing a lost update.READ COMMITTED is common with proper locking, but SERIALIZABLE gives the strongest safety when you can tolerate retries under heavy contention.account_id first, so two sessions do not wait on each other in opposite directions.UPDATE statements and no transaction? No, because a crash between them can leave the database inconsistent.COMMIT just mean “send the data to the client”? No. It means the database has made the change durable and valid according to its transaction rules.Common Mistakes:
FLOAT for money: fix this by using NUMERIC or DECIMAL.WHERE balance >= amount or a CHECK constraint.Memory Hook: “Debit, credit, commit — or admit defeat and roll back.” If both doors in the bank vault cannot close, nothing is complete.
Cheat Sheet:
BEGIN, then debit, then credit, then COMMIT.ROLLBACK if anything fails.NUMERIC and constraints.Practice Tasks: