The transfer, written down
Start every ACID answer from code, because each letter is about what happens to these lines when something goes wrong. Account A holds 500 and account B holds 200.
CREATE TABLE accounts (
id text PRIMARY KEY,
balance numeric NOT NULL CHECK (balance >= 0)
);
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;
Atomicity and consistency
Atomicity. Suppose the server crashes after the first UPDATE. Without atomicity, 100 has left A and arrived nowhere. With it, the database rolls the unfinished transaction back on recovery, so A is 500 again and B is 200. The same happens if your code hits an error and issues ROLLBACK. All or nothing, per transaction.
Consistency. Now try to transfer 600 from A. The first UPDATE would make A's balance -100, the CHECK constraint rejects it, and in PostgreSQL the transaction fails as a whole. (In MySQL InnoDB only the failing statement is rolled back, so the application must roll the transaction back itself rather than commit.) That is what the C in ACID covers: constraints the schema declares, such as primary keys, foreign keys, NOT NULL and CHECK.
It does not cover rules the database was never told. "Total money across all accounts is unchanged by a transfer" is your invariant. If a bug credits B by 1000 instead of 100, every constraint still passes and the database commits it happily.
Isolation, with real numbers
Isolation is the letter panels push hardest, because it is not all-or-nothing. Take two cash withdrawals of 100 from A at the same moment, written the way many first projects write them: read the balance into the application, subtract, write it back.
| Step | Session 1 | Session 2 | A in the table |
|---|
| 1 | SELECT balance returns 500 | | 500 |
| 2 | | SELECT balance returns 500 | 500 |
| 3 | UPDATE ... SET balance = 400 | | 400 |
| 4 | COMMIT | | 400 |
| 5 | | UPDATE ... SET balance = 400 | 400 |
| 6 | | COMMIT | 400 |
The customer took out 200 and the account dropped by 100. This is a lost update. Both sessions were inside transactions, and at the default level in PostgreSQL (READ COMMITTED) this schedule is allowed. Isolation levels exist to rule out anomalies like this one, and the default level does not rule out all of them.
Two fixes work. Write the change as one statement, SET balance = balance - 100. At READ COMMITTED the database applies it to the latest committed row under a row lock, so the second session waits if it must and then subtracts from 400. Or have both sessions lock the row as they read it with SELECT balance ... FOR UPDATE, so session 2 blocks at step 2 until session 1 commits. A conditional update, ... WHERE id = 'A' AND balance >= 100, also stops overdrafts under concurrency, because a row count of zero tells you the withdrawal was refused.
The other anomaly worth one sentence is a reader seeing half a transfer. A dirty read would let an auditor see A at 400 and B still at 200. Every level above READ UNCOMMITTED forbids that. But at READ COMMITTED an auditor who reads A and then B in two separate queries can still see A before the transfer and B after it. A single snapshot, such as REPEATABLE READ in PostgreSQL, gives a total that matches one real moment.
Durability
When COMMIT returns, the transfer has been recorded in the database's log on durable storage. If the power fails a moment later, recovery replays that log and the transfer is there. The data files themselves may be written later. The normalisation and ACID page explains why that is safe.
What isolation does not mean
A fresher answer often says isolation means "transactions run one after another". Only the SERIALIZABLE level promises a result equivalent to some one-at-a-time order. The defaults are weaker: READ COMMITTED in PostgreSQL, REPEATABLE READ in MySQL's InnoDB engine. That gap is exactly why the lost update above happens in real code.
So the honest answer to "what does isolation protect?" is "it depends on the level". Then name the anomaly each level blocks: dirty reads, non-repeatable reads, phantoms. The isolation levels page covers each one and write skew, the anomaly snapshot isolation still allows.
Show ACID on the transfer itself: what rolls back, which constraint rejects it, which interleaving loses 100, and what survives the power cut. Isolation is a dial, and the default setting does not stop every lost update.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).