Transactions & ACID Properties
The mechanism that keeps your bank balance correct when the server crashes mid-transfer — what transactions are, why they exist, and how ACID properties guarantee correctness even when everything goes wrong.
Why Transactions Exist — The Bank Transfer That Started It All
Consider the most classic scenario in all of database theory. You open Paytm and transfer ₹500 to a friend. From the database's perspective, this involves two separate operations:
Now imagine the database server crashes — power failure, OS panic, hardware fault — after Operation 1 completes but before Operation 2 executes. Your ₹500 has been deducted. Your friend received nothing. The money has vanished from the financial system. This is not a theoretical edge case — it is a real failure mode that happens in every system that operates long enough. Without a mechanism to handle it, every financial system, every e-commerce platform, every hospital record system is fundamentally unreliable.
The solution is a transaction — a mechanism that groups multiple operations into a single, indivisible unit of work. Either ALL operations in the transaction complete successfully and are permanently saved, or NONE of them take effect. Partial completion is not a possible outcome. The two updates either both happen or neither happens. The money either transfers completely or stays exactly where it was.
Transactions are not limited to financial systems. Every situation where multiple database operations must succeed or fail together requires a transaction. Placing an order and decrementing inventory. Creating a user account and sending a welcome email record. Marking a payment as complete and updating the order status. Removing a file record and deleting its metadata. Any multi-step operation with a business-level meaning requires transactional protection.
A transaction is a logical unit of work consisting of one or more database operations (reads and/or writes) that must be executed as a single, atomic, indivisible unit. A transaction takes the database from one consistent state to another consistent state. It either commits — making all its changes permanent — or aborts — undoing all its changes as if it never ran.
What Happens Without Transactions — Concrete Failure Scenarios
Before examining how transactions work, it is worth precisely cataloguing what goes wrong without them. These are not hypothetical — they are the documented failure modes that motivated transaction theory in the 1970s.
Transaction Syntax — BEGIN, COMMIT, ROLLBACK, SAVEPOINT
Every transaction in SQL follows the same lifecycle: it begins, executes operations, and either commits (making changes permanent) or rolls back (undoing all changes). The exact syntax varies slightly between databases, but the semantics are identical.
-- BEGIN (also: START TRANSACTION) — marks the start of a transaction
BEGIN;
-- In PostgreSQL, every statement is implicitly in a transaction even without BEGIN.
-- BEGIN explicitly starts a multi-statement transaction.
-- In MySQL: START TRANSACTION; or BEGIN; (both work)
-- Execute operations within the transaction
UPDATE accounts SET balance = balance - 500 WHERE account_id = 'ACC001';
UPDATE accounts SET balance = balance + 500 WHERE account_id = 'ACC002';
-- COMMIT — make all changes permanent
COMMIT;
-- After COMMIT: changes are durable, visible to all other transactions,
-- and cannot be undone by ROLLBACK.
-- ─────────────────────────────────────────────
-- ROLLBACK — undo ALL changes since BEGIN
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 'P001';
-- Something went wrong (stock went negative, payment failed, etc.)
ROLLBACK;
-- After ROLLBACK: inventory.stock is exactly what it was before BEGIN.
-- It is as if the UPDATE never executed.
-- ─────────────────────────────────────────────
-- SAVEPOINT — create a named rollback point within a transaction
BEGIN;
INSERT INTO orders (customer_id, restaurant_id, total_amount)
VALUES (1, 2, 280.00);
-- order_id = 5001 auto-assigned
SAVEPOINT after_order_created;
-- Checkpoint: order exists, we can roll back to here if needed
INSERT INTO order_items (order_id, item_id, quantity, unit_price)
VALUES (5001, 15, 2, 140.00);
-- Suppose item_id = 15 doesn't exist → FK violation
ROLLBACK TO SAVEPOINT after_order_created;
-- Only the order_items INSERT is rolled back.
-- The orders INSERT is PRESERVED — order 5001 still exists.
-- We can now try a different item or surface the error.
INSERT INTO order_items (order_id, item_id, quantity, unit_price)
VALUES (5001, 12, 2, 140.00); -- try a valid item
COMMIT; -- both the order and the corrected item are committed
-- RELEASE SAVEPOINT — explicitly remove a savepoint (frees resources)
RELEASE SAVEPOINT after_order_created;
-- After RELEASE: cannot ROLLBACK TO this savepoint anymore.
-- The transaction continues normally.Autocommit — The Default Behaviour That Surprises Beginners
Most databases operate in autocommit modeby default. In autocommit mode, every individual SQL statement is automatically wrapped in its own transaction — it begins, executes, and commits immediately. You do not need to write BEGIN/COMMIT for single statements. This is convenient for simple operations but means that if you run three UPDATE statements without BEGIN/COMMIT, each UPDATE commits independently — a failure between the second and third UPDATE leaves the database in a partially updated state with no way to undo the first two.
-- In autocommit mode (default in MySQL, PostgreSQL psql):
-- This UPDATE is automatically wrapped in a transaction and committed:
UPDATE customers SET city = 'Bengaluru' WHERE customer_id = 1;
-- Equivalent to: BEGIN; UPDATE...; COMMIT;
-- Cannot be rolled back after execution.
-- This sequence is DANGEROUS without explicit BEGIN:
UPDATE orders SET status = 'processing' WHERE order_id = 5001; -- auto-committed
UPDATE inventory SET stock = stock - 1 WHERE product_id = 'P001'; -- auto-committed
-- Server crashes here
UPDATE payments SET status = 'captured' WHERE order_id = 5001; -- NEVER EXECUTES
-- Result: order is 'processing', inventory is decremented, payment is never captured.
-- The database is in an inconsistent state with no automatic recovery.
-- CORRECT: explicit transaction groups all three as one unit
BEGIN;
UPDATE orders SET status = 'processing' WHERE order_id = 5001;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 'P001';
UPDATE payments SET status = 'captured' WHERE order_id = 5001;
COMMIT;
-- Either all three happen or none of them happen.
-- DISABLING autocommit (MySQL):
SET autocommit = 0;
-- Now every statement requires explicit COMMIT to make it permanent.
-- PostgreSQL psql: autocommit is ON by default.
-- In psql: set AUTOCOMMIT off -- disables autocommit for sessionTransaction States — The Complete Lifecycle
A transaction moves through a defined set of states from the moment it begins to the moment it terminates. Understanding these states is important for understanding what can and cannot be undone at each point — and it appears in nearly every DBMS exam.
ACID Properties — The Four Guarantees That Define a Reliable Database
ACID is an acronym coined by Andreas Reuter and Theo Härder in their 1983 paper "Principles of Transaction-Oriented Database Recovery." It describes the four properties that every reliable transaction processing system must guarantee. These four properties are not independent features — they work together as a complete system for ensuring database correctness. Remove any one and the system becomes unreliable in a specific, predictable way.
Every interview question about ACID is ultimately asking: "Do you understand what can go wrong in a multi-user, failure-prone system, and do you understand the precise mechanism that prevents each class of problem?" That is the level at which we will examine each property.
ACID in Real Production Systems — How Companies Apply These Properties
ACID properties are not abstract theory — they are engineering constraints that directly shape how production systems are built. Here is how each property manifests in real systems at Indian tech companies.
Every payment involves debiting the customer's account, crediting the merchant, creating a transaction record, and updating the payment status. All four steps must be atomic — a partial payment that debits without crediting is a legal and financial disaster. Isolation at SERIALIZABLE level prevents two concurrent requests from processing the same payment twice. Durability ensures that a payment confirmed to the customer is permanently recorded even if the server crashes 10 milliseconds after the confirmation is sent.
When a customer places an order, Swiggy must atomically: (1) create the order record, (2) deduct from inventory, (3) charge the payment, (4) notify the restaurant. These are wrapped in a transaction — if payment fails, the order and inventory changes are rolled back. They use READ COMMITTED isolation for the order listing page — it is acceptable for a customer to see an order that was created 50ms ago, and the lower locking overhead supports higher throughput.
During Big Billion Days sales with thousands of concurrent purchases of limited items, lost updates are the primary threat. Without proper isolation, two customers can simultaneously read stock=1, both think they can purchase, both write stock=0 — and both receive confirmation for an item that only has one unit. Flipkart uses SELECT FOR UPDATE on inventory rows during checkout, serialising access to each product's stock count. This causes some requests to queue, but prevents overselling.
Credit score calculations involve reading multiple tables (payment history, credit utilisation, account age) and computing a score. If a user's payment is processed between two reads in the same calculation, the score could be inconsistently computed. CRED uses REPEATABLE READ isolation for credit score transactions — all reads within the transaction see a consistent snapshot of the database as of when the transaction started, regardless of concurrent updates.
Complete Worked Example — Flipkart Checkout Transaction
This is the kind of transaction that a backend engineer at Flipkart would write for the checkout flow. Notice how every ACID property is explicitly considered in the design.
-- FLIPKART CHECKOUT TRANSACTION
-- Placing an order for 2 units of product P001 by customer C001
BEGIN;
-- ISOLATION: transaction isolation level set to REPEATABLE READ for this transaction
-- (prevents non-repeatable reads during the multi-step checkout)
-- STEP 1: Verify customer is active and eligible (CONSISTENCY check)
SELECT customer_id, is_active, credit_limit
FROM customers
WHERE customer_id = :customer_id
FOR UPDATE; -- lock this row to prevent concurrent account deactivation
-- Application checks: is_active = true, etc.
-- If inactive: ROLLBACK — consistency violation
-- STEP 2: Lock inventory and check stock (ISOLATION: prevent lost update)
SELECT product_id, stock_quantity, reserved_quantity, price
FROM inventory
WHERE product_id = :product_id
FOR UPDATE; -- exclusive lock prevents concurrent purchases from reading stale stock
-- Application checks: (stock_quantity - reserved_quantity) >= requested_quantity
-- If insufficient: ROLLBACK — consistency violation (cannot oversell)
-- STEP 3: Reserve inventory (ATOMICITY: part of the indivisible unit)
UPDATE inventory
SET reserved_quantity = reserved_quantity + :requested_qty
WHERE product_id = :product_id;
-- STEP 4: Create order record (ATOMICITY: must happen with inventory update)
INSERT INTO orders (
order_id, customer_id, status, created_at
) VALUES (
:new_order_id, :customer_id, 'payment_pending', CURRENT_TIMESTAMP
);
-- STEP 5: Create order line items (ATOMICITY: all or nothing)
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (:new_order_id, :product_id, :requested_qty, :current_price);
-- unit_price = snapshot of current price — CONSISTENCY (price may change later)
-- STEP 6: Deduct from customer wallet or create payment request
UPDATE customer_wallets
SET balance = balance - :order_total
WHERE customer_id = :customer_id
AND balance >= :order_total;
-- If no rows updated (balance insufficient): ROLLBACK — consistency violation
-- STEP 7: Create payment record
INSERT INTO payments (order_id, amount, method, status)
VALUES (:new_order_id, :order_total, :payment_method, 'captured');
-- STEP 8: Update inventory — convert reserved to actual deduction
UPDATE inventory
SET
stock_quantity = stock_quantity - :requested_qty,
reserved_quantity = reserved_quantity - :requested_qty
WHERE product_id = :product_id;
-- STEP 9: Update order status to confirmed
UPDATE orders SET status = 'confirmed' WHERE order_id = :new_order_id;
COMMIT;
-- DURABILITY: COMMIT flushes WAL to disk before returning success.
-- Even if the server crashes 1ms after this line, the entire transaction
-- (steps 1-9) will be recovered and replayed on restart.
-- The customer receives their order confirmation with guaranteed backing data.
-- ATOMICITY SUMMARY: All 9 steps succeed together or ALL are rolled back.
-- If step 6 (wallet deduction) fails, steps 1-5 are undone — inventory released.
-- If server crashes after step 7, all steps are rolled back on restart.
-- WHAT HAPPENS ON ROLLBACK:
-- inventory.reserved_quantity returns to original value
-- order is deleted (never committed)
-- order_items are deleted (never committed)
-- wallet balance returns to original (never committed)
-- Customer sees: "Payment failed, please try again" — clean stateBASE — The Alternative Consistency Model for Distributed Systems
ACID is the consistency model of traditional relational databases. When databases are distributed across multiple servers (as covered in Module 17), maintaining full ACID guarantees becomes extremely expensive — it requires coordination between servers which adds latency and reduces throughput. Many internet-scale systems opt for a weaker consistency model called BASE.
Atomic — all or nothing
Consistent — always valid state
Isolated — concurrent = serial
Durable — committed = permanent
Used by: PostgreSQL, MySQL, Oracle, SQL Server
Best for: financial systems, inventory, anything where correctness is mandatory
Basically Available — system always responds (may return stale data)
Soft state — data may change over time even without input (replication catching up)
Eventually Consistent — given no new updates, all replicas will converge to the same value
Used by: Cassandra, DynamoDB, CouchDB, many NoSQL systems
Best for: social feeds, product catalogs, user preferences — data where slight staleness is acceptable
BASE does not mean "no consistency" — it means eventual consistency: the system will eventually reach a consistent state, but at any given moment, different servers might serve slightly different data because replication has not yet propagated the latest changes to all nodes. A social media like count that shows 1,423 on one server and 1,421 on another — both will converge to 1,424 eventually. For a Like count this is acceptable. For a bank balance it absolutely is not.
The Debugging Session — Finding a Missing Transaction in Production Code
This scenario plays out in every backend team. A subtle bug creates data inconsistency in production. The root cause is always a missing transaction boundary. Understanding this scenario deeply is what makes you the engineer who finds and fixes it before it causes a major incident.
Customer support escalates: "Customers report that after a failed payment, their order appears in the system with status 'payment_failed' but the order_items are missing. The order total shows ₹0."
# Python Flask backend — checkout endpoint (buggy version)
@app.route('/checkout', methods=['POST'])
def checkout():
data = request.json
# Step 1: Create order record
order_id = db.execute("""
INSERT INTO orders (customer_id, restaurant_id, total_amount, status)
VALUES (%s, %s, %s, 'payment_pending')
RETURNING order_id
""", (data['customer_id'], data['restaurant_id'], data['total_amount']))
# ← AUTO-COMMITTED immediately (no BEGIN)! Order exists in DB.
# Step 2: Insert order items
for item in data['items']:
db.execute("""
INSERT INTO order_items (order_id, item_id, quantity, unit_price)
VALUES (%s, %s, %s, %s)
""", (order_id, item['id'], item['qty'], item['price']))
# ← Each INSERT AUTO-COMMITTED immediately!
# Step 3: Process payment via external API
try:
payment_result = payment_gateway.charge(
amount=data['total_amount'],
card_token=data['card_token']
)
except PaymentFailedException as e:
# Payment failed — update order status
db.execute("""
UPDATE orders SET status = 'payment_failed', total_amount = 0
WHERE order_id = %s
""", (order_id,))
# ← Updates order to failed state
# BUT: order_items from Step 2 were already committed and remain in DB
# total_amount set to 0 but items still exist
return {'error': 'Payment failed'}, 400
return {'order_id': order_id}, 200
# BUG: The order and order_items are committed even when payment fails.
# total_amount is reset to 0 but line items remain — inconsistent state.# Python Flask backend — checkout endpoint (FIXED with transaction)
@app.route('/checkout', methods=['POST'])
def checkout():
data = request.json
try:
# START TRANSACTION — all operations are now part of one atomic unit
db.execute("BEGIN")
# Step 1: Create order record
order_id = db.execute("""
INSERT INTO orders (customer_id, restaurant_id, total_amount, status)
VALUES (%s, %s, %s, 'payment_pending')
RETURNING order_id
""", (data['customer_id'], data['restaurant_id'], data['total_amount']))
# NOT committed — held in transaction buffer
# Step 2: Insert order items
for item in data['items']:
db.execute("""
INSERT INTO order_items (order_id, item_id, quantity, unit_price)
VALUES (%s, %s, %s, %s)
""", (order_id, item['id'], item['qty'], item['price']))
# NOT committed — held in transaction buffer
# Step 3: Process payment
payment_result = payment_gateway.charge(
amount=data['total_amount'],
card_token=data['card_token']
)
# Payment succeeded — now commit everything together
db.execute("COMMIT")
# COMMIT: order + order_items become permanent simultaneously
return {'order_id': order_id}, 200
except PaymentFailedException as e:
db.execute("ROLLBACK")
# ROLLBACK: order + order_items are UNDONE — as if they never existed
# No orphaned records, no inconsistent state
return {'error': 'Payment failed, no charges made'}, 400
except Exception as e:
db.execute("ROLLBACK")
# Any unexpected error: clean rollback
return {'error': 'Checkout failed'}, 500
# WITH TRANSACTION:
# - Payment fails → ROLLBACK → zero records in DB → customer can retry cleanly
# - Payment succeeds → COMMIT → order + items both permanently exist
# - Server crash mid-operation → auto-ROLLBACK on restart → clean stateThe fix is nine characters: adding BEGIN at the start, COMMIT on success, and ROLLBACK on failure. Those nine characters are the difference between a system that leaves orphaned, inconsistent records on payment failure and one that maintains perfect data integrity. This class of bug is responsible for a significant percentage of data quality incidents in production systems — and it is found in codebases at companies of every size.
Transaction and ACID Interview Questions — Complete Answers
Use the bank transfer: Atomicity — both the debit and credit happen together or neither happens; the money cannot vanish mid-transfer. Consistency — both accounts must satisfy their constraints before and after (balance cannot go negative if a CHECK constraint exists; the total money in the system is conserved). Isolation — if two transfers happen simultaneously, each transfer sees a consistent state; the intermediate state of one is not visible to the other. Durability — once the transfer is confirmed, it survives any subsequent failure; the committed changes are written to durable storage and recovered after a crash. These four properties together make the bank transfer reliable even in a system with failures and concurrent users.
Atomicity deals with the outcome of a transaction: either all operations complete or none do. It is about preventing partial completion. It applies during the transaction's execution and determines what happens on failure before commit. Durability deals with what happens after a successful commit: the committed changes persist through any subsequent failure. It is about ensuring committed data is not lost. A transaction can be atomic (never partially committed) without being durable (committed data can still be lost if it was in RAM and not flushed to disk when the power failed). Durability specifically addresses the gap between acknowledging a commit and physically writing it to non-volatile storage.
Consistency is about data validity — ensuring that integrity constraints, business rules, and data invariants are satisfied before and after every transaction. It is a property of the database state at transaction boundaries. A consistent transaction moves the database from one valid state to another. Isolation is about transaction visibility — ensuring that concurrent transactions do not see each other's intermediate (incomplete) states. It is a property of how multiple transactions interact during their concurrent execution. A database can be consistent (all constraints satisfied) while not being isolated (transactions seeing each other's uncommitted changes). They address completely orthogonal concerns.
Nothing bad — this is exactly what Durability guarantees. When COMMIT returns to the application, the database has already written the commit record to the Write-Ahead Log and flushed it to disk (with synchronous_commit = on). The actual data pages may still be in the buffer pool in RAM, but the WAL on disk contains a complete record of all changes. On restart, the recovery system reads the WAL, identifies the committed transaction (by finding its commit record), and re-applies (redoes) all its changes to the data pages. The committed data is fully recovered. The application can be confident that a committed transaction is permanent.
READ COMMITTED: a transaction only sees data that has been committed before each individual statement executes. If another transaction commits between two statements in the same transaction, the second statement sees the new data. This allows non-repeatable reads — reading the same row twice in one transaction can return different values. REPEATABLE READ: a transaction sees a consistent snapshot of the database as of the moment the transaction began. Even if other transactions commit changes to rows being read, the repeatable read transaction always sees the original values it first read. Non-repeatable reads are impossible. However, phantom reads (new rows appearing that match a previous query) are still possible in some implementations (though PostgreSQL's REPEATABLE READ actually prevents phantoms too using snapshot isolation). PostgreSQL default is READ COMMITTED. MySQL default is REPEATABLE READ.
Yes. A COMMIT can fail in several scenarios. Disk failure during WAL write: the commit record cannot be written to durable storage. The DBMS cannot guarantee durability, so the commit fails and the transaction is rolled back. Constraint violation detected at commit time: some constraints (deferred constraints in PostgreSQL) are checked at commit rather than immediately. If a violation is detected at commit time, the commit fails and the transaction is aborted. Out of disk space: if the WAL or data files cannot be written because disk is full, the commit fails. In all cases, a failed commit results in the transaction being aborted and all its changes being rolled back. The application receives an error rather than a success confirmation. No partial commits are possible.
ROLLBACK (without SAVEPOINT) aborts the entire transaction and undoes ALL changes since BEGIN. The transaction terminates and cannot continue. A new transaction must be started if needed. ROLLBACK TO SAVEPOINT undoes only the changes made after the specified SAVEPOINT was created. The transaction continues — it does NOT terminate. Changes made before the SAVEPOINT are preserved and can still be committed. The savepoint itself is preserved (you can roll back to it again). RELEASE SAVEPOINT removes a savepoint but the transaction continues. Savepoints are useful for partial error recovery within a long transaction: try a risky operation, and if it fails, rollback to the savepoint and try an alternative approach without losing the work done before the savepoint.
🎯 Key Takeaways
- ✓A transaction is a logical unit of work that must execute as a single indivisible unit — all operations succeed together or all are rolled back together. Partial completion is never a valid outcome.
- ✓Transaction lifecycle: Active (executing) → Partially Committed (last operation done, waiting for disk write) → Committed (permanent) or Failed → Aborted (fully rolled back). A failed transaction cannot resume — it must restart completely.
- ✓Autocommit mode wraps each SQL statement in its own implicit transaction. Multi-step operations MUST use explicit BEGIN/COMMIT to be grouped atomically. Missing BEGIN is the most common cause of data inconsistency bugs in production.
- ✓SAVEPOINT creates a named rollback point within a transaction. ROLLBACK TO SAVEPOINT undoes changes since that savepoint without terminating the transaction. The transaction can continue and eventually COMMIT or fully ROLLBACK.
- ✓Atomicity: all or nothing — implemented via Write-Ahead Logging (WAL). Before any data page is changed, the change is recorded in the WAL and flushed to disk. On crash recovery, the WAL is replayed to restore committed state.
- ✓Consistency: every transaction moves the database from one valid state to another. DBMS enforces structural constraints (PK, FK, CHECK, NOT NULL). Application must enforce semantic business rules (balance cannot go negative, order total must match items).
- ✓Isolation: concurrent transactions do not see each other's intermediate states. Four isolation levels: READ UNCOMMITTED (no protection), READ COMMITTED (prevents dirty reads — PostgreSQL default), REPEATABLE READ (prevents non-repeatable reads — MySQL default), SERIALIZABLE (prevents all anomalies — maximum correctness).
- ✓Four concurrency anomalies: Dirty Read (reading uncommitted changes), Non-Repeatable Read (same row returns different values in same transaction), Phantom Read (new rows appear in repeated query), Lost Update (concurrent writes overwrite each other). Each isolation level prevents a different subset.
- ✓Durability: once COMMIT returns, the transaction is permanent through any failure — implemented by flushing WAL to disk before acknowledging COMMIT. synchronous_commit = on (default) guarantees this. synchronous_commit = off trades durability for speed.
- ✓BASE (Basically Available, Soft state, Eventually Consistent) is the alternative to ACID for distributed systems where strong consistency is too expensive. Use ACID for financial, inventory, and transactional data. Use BASE for social, analytics, and high-volume low-stakes data where eventual consistency is acceptable.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.