1. Home
  2. Blog
  3. Databases & SQL

Databases & SQL

SQL Transactions and ACID Explained: COMMIT, ROLLBACK and Isolation Levels

What happens if the server fails halfway through moving money between two accounts? Transactions are the answer. See how they work on a real example, what ACID means and how to choose an isolation level.

9 min read
SQL transactions and ACID explained: COMMIT, ROLLBACK and isolation levels

Key takeaways

  • A transaction groups statements so that they all succeed together or all fail together.
  • COMMIT makes the changes permanent, and ROLLBACK cancels them.
  • ACID stands for atomicity, consistency, isolation and durability.
  • Stronger isolation prevents more anomalies, but it can reduce concurrency, so choose deliberately.

Moving 300 rupees from Asha to Ben takes two updates: subtract from one account and add to the other. If the system fails between them, money vanishes. SQL transactions exist to make that impossible, by treating both steps as a single unit.

In this guide we run through commits, rollbacks and failures on a small accounts table. We ran those examples in SQLite, and the balances shown come from that run. The basic commands are similar across databases, but locking and isolation behaviour vary a lot, so we point out where to check your own database’s documentation.

What is a transaction?

A transaction is a sequence of one or more SQL statements that the database treats as a single logical operation. Here is our table:

CREATE TABLE accounts (
  id INT PRIMARY KEY,
  owner TEXT,
  balance INT CHECK (balance >= 0)
);

INSERT INTO accounts VALUES (1, 'Asha', 1000), (2, 'Ben', 200);

The CHECK constraint is a rule that a balance can never go below zero, which we will use later. Note that the syntax to begin a transaction varies: SQLite and PostgreSQL accept BEGIN, MySQL also has START TRANSACTION, and SQL Server uses BEGIN TRANSACTION.

Outside an explicit transaction, most databases use autocommit, where each statement is saved as soon as it finishes. Oracle is a notable exception, because a session there stays in a transaction until you commit.

COMMIT: make it permanent

BEGIN;
UPDATE accounts SET balance = balance - 300 WHERE id = 1;
UPDATE accounts SET balance = balance + 300 WHERE id = 2;
COMMIT;

-- Asha | 700
-- Ben  | 500

Until COMMIT runs, the changes are provisional. Once it returns successfully, the database guarantees that they are saved. The total across both accounts stayed at 1200, which is the whole point of the exercise.

ROLLBACK: cancel everything

ROLLBACK undoes everything since the transaction began:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SELECT owner, balance FROM accounts;
-- Asha | 600   (visible inside this transaction)
-- Ben  | 500

ROLLBACK;
SELECT owner, balance FROM accounts;
-- Asha | 700   (back to where it was)
-- Ben  | 500

A rollback also happens automatically if the connection drops or the server crashes before you commit, so a half-finished transaction does not survive.

When a statement fails

Here is a more realistic case. Asha tries to send 5000, which the account does not hold. The credit to Ben runs first, and then the debit breaks the CHECK rule:

BEGIN;
UPDATE accounts SET balance = balance + 5000 WHERE id = 2;
UPDATE accounts SET balance = balance - 5000 WHERE id = 1;
-- error: CHECK constraint failed: balance >= 0

ROLLBACK;

-- Asha | 700
-- Ben  | 500

Without the transaction, Ben would have kept the 5000 and the books would be wrong. Be aware of one important detail: a failing statement does not always cancel the transaction by itself. In SQLite and MySQL, for example, earlier statements can remain pending, so your code has to issue the rollback. PostgreSQL instead marks the whole transaction as aborted and refuses further statements until you roll back.

In application code, always pair the transaction with error handling. Here is the idea in Python, and our guide to Python exception handling explains the syntax:

try:
    conn.execute("BEGIN")
    conn.execute(debit_sql, values)
    conn.execute(credit_sql, values)
    conn.execute("COMMIT")
except Exception:
    conn.execute("ROLLBACK")
    raise

Savepoints

A savepoint marks a position inside a transaction, so you can undo only part of the work:

BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
SAVEPOINT bonus;
UPDATE accounts SET balance = balance + 999 WHERE id = 2;
ROLLBACK TO bonus;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;

-- Asha | 650
-- Ben  | 550

The 999 bonus was undone, while the rest of the transaction carried on and committed. This is useful for optional steps in a long process.

ACID properties explained

Property Promise In our transfer
Atomicity All or nothing Both updates happen, or neither does
Consistency Rules still hold afterwards No balance can break the CHECK rule
Isolation Concurrent work does not interfere Others do not see a half-finished transfer
Durability Committed data survives failure A power cut after COMMIT loses nothing

Consistency is commonly misunderstood. The database enforces only the rules you declare, such as constraints and foreign keys. It will not stop your code from transferring the wrong amount. Durability is usually achieved with a write-ahead log, where changes are recorded safely on disk before the commit is confirmed.

What happens with concurrent transactions?

We opened two connections to one SQLite file. The first started a transaction and updated a row from 1 to 2, without committing yet. Here is what the second connection saw:

Connection B reads:   1                     -- no uncommitted data
Connection B writes:  database is locked    -- it has to wait
(A commits)
Connection B reads:   2                     -- now it sees the change

SQLite allows only one writer at a time, which is simpler than server databases, where many transactions can write different rows simultaneously. But the principle is the same: other sessions should not see uncommitted changes under normal settings, and conflicting writes have to wait. In our test, we set the second connection to give up immediately rather than wait, so the error appeared at once.

Read anomalies

When transactions overlap, certain problems can occur. The SQL standard defines these in terms of what a transaction may observe:

Anomaly What happens
Dirty read You read a change another transaction has not committed, and it may later be rolled back
Non-repeatable read You read a row twice in one transaction and get different values, because someone committed in between
Phantom read You run the same query twice and get extra or missing rows, because someone inserted or deleted some

Another problem is the lost update. Two sessions read a balance of 100, each adds 10 and each writes 110. One update has effectively vanished, when the answer should be 120.

SQL isolation levels

Isolation levels let you trade strictness for concurrency. Here is what the SQL standard allows at each level:

Level Dirty read Non-repeatable read Phantom read
Read uncommitted Possible Possible Possible
Read committed Prevented Possible Possible
Repeatable read Prevented Prevented Possible
Serializable Prevented Prevented Prevented
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

The exact syntax and behaviour depend on your database. Defaults also differ: PostgreSQL and SQL Server use read committed, while MySQL’s InnoDB uses repeatable read. Some databases give stronger guarantees than the standard requires, or treat one level as another. In PostgreSQL, for example, read uncommitted behaves like read committed. Always read your database’s documentation before relying on a specific behaviour.

Tip: For a lost update, make the change in a single statement, as in UPDATE accounts SET balance = balance + 10. Doing the calculation in your application from an earlier read is what opens the gap.

Deadlocks

A deadlock happens when two transactions each hold a lock the other one needs. Transaction A locks row 1 and wants row 2, while transaction B locks row 2 and wants row 1. Neither can continue.

Databases such as PostgreSQL, MySQL and SQL Server detect this and cancel one transaction with an error. That is normal behaviour, and your application should be prepared for it. To reduce deadlocks, always update rows in a consistent order, such as lowest account ID first, keep transactions short, and retry when one is chosen as the victim.

Best practices

  • Keep transactions short. Long ones hold locks and block other users.
  • Never wait for a user or a slow API inside a transaction. Do that work before you begin, or afterwards.
  • Always handle failure. Every path should end in a commit or a rollback.
  • Add constraints. They give the database real rules to enforce. Good table design helps too.
  • Use good indexes. Faster lookups mean that locks are held for less time.
  • Be careful with retries. Retry on deadlocks, but make sure that an operation cannot apply twice.
  • Test concurrency on purpose. Many of these bugs appear only under load, so basic integration tests alone may not reveal them.

Final thoughts

Transactions turn several risky steps into one safe operation, and ACID describes the guarantees that make this trustworthy. Whenever a business action touches more than one row or table, ask yourself what would happen if it stopped halfway. If the answer worries you, wrap it in a transaction. To practise, open two sessions in a local database and try to produce the anomalies yourself.

Frequently asked questions

What is a transaction in SQL?

A transaction is a group of SQL statements treated as one unit. Either all of them are saved with COMMIT, or all of them are undone with ROLLBACK.

What are ACID properties in a database?

ACID stands for atomicity, consistency, isolation and durability. Together, they ensure transactions are all or nothing, keep rules intact, do not interfere and survive failures.

What is the difference between COMMIT and ROLLBACK?

COMMIT permanently saves all changes made in the transaction. ROLLBACK cancels them and returns the data to how it was before the transaction began.

What is a dirty read?

A dirty read is when one transaction reads changes another has not committed yet. If that other transaction rolls back, the data that was read never truly existed.

Which isolation level should I use?

Start with your database’s default, which is usually fine. Use serializable for critical logic where anomalies are unacceptable, and be ready to retry failed transactions.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Software Development

Python Dataclasses Explained: Less Boilerplate, Safer Defaults and Real Examples

Python dataclasses explained with code we ran: generated methods, default_factory, frozen and ordered classes, __post_init__, slots, kw_only and when to…

Read article →
Software Development

JavaScript Destructuring Explained: Objects, Arrays, Defaults and Common Errors

JavaScript destructuring explained with code we ran in Node.js: objects, arrays, renaming, defaults, nested patterns, rest, function parameters and the…

Read article →
Software Development

Python f-strings Explained: Formatting Numbers, Dates and Debug Output

Python f-strings explained with code we ran: format specifiers for numbers, alignment and dates, the = debug shortcut, rounding surprises,…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
DevOps

Environment Variables Explained: .env Files, Secrets and Config Done Right

Environment variables explained with Python examples we ran: reading and setting them, why every value is a string, .env files,…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Databases & SQL

DELETE vs TRUNCATE vs DROP in SQL: Differences, Rollback and When to Use Each

DELETE vs TRUNCATE vs DROP explained with tested SQL: what each removes, rollback, triggers, identity reset, speed, and how PostgreSQL,…

Read article →
Databases & SQL

SQL Views Explained: Create, Use and Avoid the Pitfalls, With Examples

SQL views explained with queries we ran: creating a view, live results, hiding columns, performance, updatable and materialized views, broken…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
DevOps

Monitoring vs Observability: The Real Difference, With Logs, Metrics and Traces

Monitoring vs observability explained: known and unknown failures, logs, metrics and traces, a checkout incident walkthrough, golden signals, OpenTelemetry and…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →
Software Testing

Test Case vs Test Scenario: Key Differences, Examples and When to Use Each

Test case vs test scenario explained: clear definitions, a side-by-side table, one password reset feature taken from scenarios to cases,…

Read article →
Artificial Intelligence

What Is a Large Language Model? How LLMs Work, Explained Without the Hype

What is a large language model? LLMs explained in plain language: tokens, embeddings, attention, training, temperature and context windows, a…

Read article →
Test Automation

Selenium vs Playwright: Key Differences, Code Compared and Which to Choose

Selenium vs Playwright compared: architecture, waiting, browser and language support, debugging, the same test in both tools, migration advice and…

Read article →