Transactions and ACID properties
Concept
A transaction is a unit of work that must be treated as a whole — either all operations succeed, or all are rolled back. Transactions enforce the ACID properties.
ACID:
- A — Atomicity: All operations in the transaction succeed or all fail. No partial updates. If the server crashes mid-transaction, the rollback is automatic on restart.
- C — Consistency: A transaction brings the database from one valid state to another valid state. Foreign key constraints, check constraints, and unique constraints are always upheld.
- I — Isolation: Concurrent transactions behave as if they ran serially. Changes made by one transaction are not visible to others until committed (at default isolation levels). Controlled by isolation level.
- D — Durability: Once committed, the data is permanently stored — even if the server crashes. Achieved via Write-Ahead Log (WAL) / binary log.
Atomicity example: Transfer $100 from Account A to Account B. Two statements: UPDATE accounts SET balance = balance - 100 WHERE id = A and UPDATE accounts SET balance = balance + 100 WHERE id = B. If the second fails, the first must be rolled back. Without a transaction: A loses $100, B gains nothing.
Savepoints: Named points within a transaction. You can rollback to a savepoint without rolling back the entire transaction. Useful for nested operations.
Auto-commit: By default, each SQL statement is its own transaction (auto-committed). BEGIN/START TRANSACTION disables auto-commit until COMMIT or ROLLBACK.
Code Example
-- Basic transaction
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Check if both updates look correct
SELECT id, balance FROM accounts WHERE id IN (1, 2);
COMMIT; -- make permanent
-- or ROLLBACK; -- undo everything since START TRANSACTION
-- Savepoint — rollback to a point without losing earlier work
START TRANSACTION;
INSERT INTO orders (id, customer_id, total) VALUES (101, 5, 250.00);
SAVEPOINT order_inserted;
INSERT INTO order_items (order_id, product_id, quantity) VALUES (101, 99, 2);
-- Assume this product doesn't exist → FK violation
ROLLBACK TO SAVEPOINT order_inserted; -- only undoes the order_items insert
-- Order 101 is still in the transaction, order_items rolled back
COMMIT;<?php
// Laravel — DB::transaction() (auto-commit on success, auto-rollback on exception)
DB::transaction(function () {
$order = Order::create([...]);
foreach (request('items') as $item) {
$order->items()->create($item);
}
Payment::charge($order->total); // throws on failure → auto-rollback
});
// Manual transaction
DB::beginTransaction();
try {
$from = Account::lockForUpdate()->findOrFail($request->from_id);
$to = Account::lockForUpdate()->findOrFail($request->to_id);
if ($from->balance < $request->amount) {
throw new \DomainException('Insufficient funds');
}
$from->decrement('balance', $request->amount);
$to->increment('balance', $request->amount);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
// Nested transactions in Laravel use savepoints automatically
DB::transaction(function () {
Order::create([...]);
DB::transaction(function () { // creates a SAVEPOINT internally
OrderItem::create([...]);
}); // RELEASE SAVEPOINT on success, ROLLBACK TO SAVEPOINT on failure
});