Intermediate · SQL in depth · topic 25 of 54 · 20 min
Transactions and ACID
Explanation
All or nothing
Sending 750 rupees from Ravi to Meera needs two updates: take 750 from Ravi, add 750 to Meera. If the server crashes after the first update, Ravi has lost money and Meera got nothing. A transaction makes the two updates one unit: either both are saved, or neither is.
BEGIN;
UPDATE accounts SET balance = balance - 750 WHERE id = 1; -- debit Ravi
UPDATE accounts SET balance = balance + 750 WHERE id = 2; -- credit Meera
COMMIT;ROLLBACK cancels everything since BEGIN, as if it never happened. Your backend code sends ROLLBACK when anything goes wrong: a check fails, an error is thrown, or the balance is too low.
BEGIN;
UPDATE accounts SET balance = 0; -- oops, forgot WHERE
ROLLBACK; -- all balances are back
SELECT holder, balance FROM accounts;ACID in simple words
| Letter | Means | In the UPI example |
|---|---|---|
| A | Atomic: all or nothing | Debit and credit both happen, or neither |
| C | Consistent: rules always hold | A balance never goes below zero |
| I | Isolated: transactions do not see each other's half-done work | Another query never sees money "in the air" |
| D | Durable: once committed, it survives a crash or power cut | After "Payment successful", the money stays moved |
Constraints guard the data
A CHECK constraint is a rule the database tests on every insert and update. With CHECK (balance >= 0), an update that would make a balance negative fails with an error. Your code catches the error and sends ROLLBACK, so the debit is undone too. The rule lives in the database, so no bug in any app can break it.
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
holder TEXT NOT NULL,
upi_id TEXT UNIQUE NOT NULL,
balance INTEGER NOT NULL CHECK (balance >= 0)
);Isolation, briefly
Many users hit the database at once. Isolation decides what one transaction can see of another. PostgreSQL by default uses READ COMMITTED: you never see uncommitted changes. For the "last seat on the train" problem, lock the row you are about to change with SELECT ... FOR UPDATE (PostgreSQL), or use the stricter SERIALIZABLE level, so two people cannot both book it.
Deadlocks
A deadlock happens when transaction A has locked Ravi's row and waits for Meera's, while transaction B has locked Meera's and waits for Ravi's. Neither can move. The database notices, cancels one of them with an error, and the app should simply retry it. You avoid most deadlocks by always locking rows in the same order, for example lower account id first, and by keeping transactions short.
Watch the lesson
Transactions and ACID · 1/7
Moving money is two steps: a debit and a credit. A crash in the middle must never lose money.
Prefer a video by a teacher? Find free videos on “database transactions acid explained commit rollback” on YouTube ↗
In real life: A wallet transfer in Node.js
A wallet app moves money with a transaction. The backend takes one database connection, sends BEGIN, runs the debit and the credit, inserts a row into a transfers log, and sends COMMIT. If any step throws (for example the CHECK constraint fails because the balance is too low), the catch block sends ROLLBACK and the user sees "Insufficient balance" with nothing changed.
async function transfer(pool, fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
await client.query('INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)', [fromId, toId, amount]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}Practice (0/3 done)
Table: accounts(id, holder, upi_id, balance). Ravi Kumar (id 1) sends 750 rupees to Meera Joshi (id 2). Inside the transaction, write the debit and the credit. The final SELECT shows the balances.
Someone started a transaction and ran an UPDATE without WHERE, setting every balance to 0. Before the final SELECT, add the one statement that cancels the whole transaction, so the original balances come back.
In a transfer transaction, the debit succeeds but the credit fails with an error. What should the backend do?
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.