Lecture 5 of 1418 minInteractiveFree lecture

Transactions and ACID

A money transfer is two updates that must both happen or neither. Watch a server crash in the middle of a transfer — with and without a transaction — and learn what atomicity, consistency, isolation and durability guarantee.

In this lecture you will

  • Explain why multi-step changes need transactions
  • Describe each of the ACID guarantees with an example
  • Explain how a write-ahead log makes commits durable and enables crash recovery
  • Use BEGIN, COMMIT and ROLLBACK correctly in application code

Some changes only make sense together. Moving ₹5,000 from Asha to Bilal is two updates: subtract from one balance, add to the other. If the first succeeds and the second doesn’t, money has vanished from the system. Crashes, network failures and bugs make that not just possible but, at scale, certain. Transactions are how databases make multi-step changes safe.

A transaction in SQL

BEGIN;
UPDATE accounts SET balance = balance - 5000 WHERE id = 'asha';
UPDATE accounts SET balance = balance + 5000 WHERE id = 'bilal';
COMMIT;

Between BEGIN and COMMIT, the changes are provisional. COMMIT makes them permanent and visible to others, all at once. ROLLBACK — or any failure before the commit — throws them all away.

Try it on a real database. This SQLite playground has the two accounts and a rule that no balance may go below zero. Run the transfer twice, break the rule inside a transaction, then do the same without one — and keep checking the total:

InteractiveTransactions on a real databasePick an example · Run · check Balances
Loading visual…

When a statement fails inside a transaction, the playground issues ROLLBACK — exactly what your application code should do. SQLite loads only when you press Run (about 0.7 MB).

Watch a crash

Step through the same transfer twice: first as two independent updates, then as a transaction. Keep an eye on the total amount of money.

InteractiveA transfer, a crash and recovery
Loading visual…
  1. Two accountsAsha has ₹8,000 and Bilal ₹2,000 — ₹10,000 in total. We want to move ₹5,000 from Asha to Bilal.
  2. Without a transactionThe first update runs on its own and is written straight away: Asha now has ₹3,000.
  3. A crash at the worst momentThe server fails before the second update. Asha is debited, Bilal is never credited, and ₹5,000 has disappeared.
  4. With a transactionAfter BEGIN, each change is first recorded in the write-ahead log — what changed, and how to undo it — before it becomes permanent.
  5. Crash and recoverThe server crashes again mid-transfer. On restart, recovery reads the log, sees no COMMIT for this transaction and rolls its change back. Balances are exactly as before.
  6. CommitThe retried transfer debits, credits and commits. The commit record is flushed to disk before success is reported — now nothing can undo it.
  7. IsolationWhile the transfer was in progress, other sessions reading the balances saw the old committed values, never a state where the money was missing.

The four ACID guarantees

Atomicity
All the changes in a transaction happen, or none do. There is no half-finished transfer.
Consistency
A transaction takes the database from one valid state to another. Constraints — foreign keys, CHECK rules, unique keys — are enforced, and violating ones abort the transaction.
Isolation
Concurrent transactions don’t see each other’s unfinished work, and behave as if they ran one after another — to a degree set by the isolation level.
Durability
Once committed, changes survive crashes and power failures.

How databases keep the promise

The key mechanism is the write-ahead log (WAL). Before changing data pages, the database appends a description of the change to a sequential log on disk. Committing means writing — and flushing to durable storage — a commit record. After a crash, the database replays the log: it redoes committed changes that hadn’t reached the data files, and undoes anything without a commit. Sequential log writes are fast, which is why databases can promise durability without writing every data page immediately.

Isolation is provided by locks and by multi-version concurrency control (MVCC), where readers see a consistent snapshot while writers create new versions of rows. Module 2 covers isolation levels and the anomalies each one allows.

Isolation is not automatic protection from every race, though. The most common concurrency bug in business software is the lost update: two requests read the same value, each computes a new one in application code, and the second write silently erases the first:

AnimationThe lost update, and two fixes
Loading animation…
  1. Two withdrawals at onceAsha’s balance is ₹100. Her phone and her laptop each send a ₹30 withdrawal at the same moment. The right final balance is ₹40.
  2. Both readEach request runs SELECT and gets ₹100. The value now lives in two separate pieces of application memory.
  3. A writes and commitsRequest A computes 100 − 30 = 70 in application code, writes 70 and commits. So far, so good.
  4. B overwrites ARequest B also computed 70 — from its stale read — and writes it. Both withdrawals succeeded, but the balance fell only once: a lost update.
  5. Fix 1: let the database do the arithmeticUPDATE … SET balance = balance − 30 reads and writes in one statement. A’s update locks the row, B’s waits, then subtracts from the committed ₹70.
  6. Fix 2: lock the row when readingSELECT … FOR UPDATE takes the row lock at read time, so B’s read waits until A commits and then sees ₹70. In PostgreSQL, the stricter REPEATABLE READ and SERIALIZABLE levels would instead abort B with an error, for the application to retry.

Shown with PostgreSQL’s default isolation level, READ COMMITTED. Most databases default to a level that allows this bug.

Transactions in application code

with connection:                       # BEGIN … COMMIT, or ROLLBACK on error
    with connection.cursor() as cur:
        cur.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (5000, "asha"))
        cur.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (5000, "bilal"))
  • Group changes that belong together in one transaction, and let an exception trigger a rollback.
  • Keep transactions short. Never hold one open while calling a slow external API or waiting for a user.
  • Let the database enforce rules. A CHECK (balance >= 0) constraint protects every program that touches the table, not just the one that remembered to validate.

That completes the first module: the relational model, SELECT, joins, indexes and transactions — the core of working confidently with any SQL database. Module 2 moves on to designing schemas, reading query plans, isolation levels, replication and NoSQL.

Key takeaways

  • A transaction groups changes so they succeed or fail together.
  • Atomicity, consistency, isolation and durability protect data from crashes, bad data and concurrent users.
  • Databases write changes to a log first; after a crash, they replay committed work and undo the rest.
  • Keep transactions short, handle errors by rolling back, and let the database enforce the rules.

Check your understanding

3 questions · answers explained

Q1A transfer debits Asha, then the server crashes before crediting Bilal. With a transaction, what happens?

Atomicity means all or nothing. Without a commit record, recovery treats the transaction as never having happened and undoes its changes.

Q2What does durability guarantee?

A database reports a commit as successful only after the commit record is safely on durable storage. From then on, a crash can’t lose the transaction.

Q3Which is a good practice for transactions in application code?

Long transactions hold locks and block other users. Short transactions that roll back on any error keep data correct and the database responsive.

You’ve finished the free lectures.

Continue with the remaining 9 lectures of Databases & SQL — one-time purchase of $49₹2,999.