Transactions — DB::transaction(), beginTransaction(), commit(), rollback()
Concept
Database transactions ensure that a group of operations either all succeed or all fail atomically. If any operation fails, the entire transaction is rolled back, leaving the database in its prior state. Transactions enforce the A (Atomicity) and I (Isolation) properties of ACID.
DB::transaction(Closure $callback, int $attempts = 1): Wraps a closure in a transaction. If an exception is thrown inside, the transaction is automatically rolled back and the exception re-thrown. If the closure returns normally, the transaction commits. $attempts enables automatic retry on deadlocks.
Manual transaction control:
DB::beginTransaction(): Start a transaction.DB::commit(): Commit all changes.DB::rollBack(): Undo all changes sincebeginTransaction.
Nested transactions (savepoints): Laravel tracks nesting level. beginTransaction inside an existing transaction creates a savepoint. rollBack() within a nested transaction rolls back to the savepoint, not the outer transaction. commit() of a nested transaction releases the savepoint.
Deadlocks and $attempts: When two transactions lock each other's rows, the database kills one (a deadlock). The $attempts parameter in DB::transaction($cb, 3) automatically retries up to 3 times on deadlock. Ensure the closure is idempotent (safe to run multiple times) when using retries.
Events in transactions: Model events fire when methods are called. If the transaction rolls back, events have already been dispatched. Consider deferring side effects (email, queue job dispatch) until after the transaction commits — use DB::afterCommit().
Code Example
<?php
use Illuminate\Support\Facades\DB;
// Automatic transaction — commits on success, rolls back on exception
DB::transaction(function() {
$order = Order::create(['user_id' => 1, 'total' => 99.99]);
$order->items()->createMany([
['product_id' => 1, 'quantity' => 2, 'price' => 29.99],
['product_id' => 2, 'quantity' => 1, 'price' => 39.99],
]);
DB::table('products')->whereIn('id', [1, 2])->decrement('stock');
// If any of the above throws, everything is rolled back
});
// With retry on deadlock
DB::transaction(function() {
// ... transfer funds between accounts ...
DB::table('accounts')->where('id', $fromId)->decrement('balance', $amount);
DB::table('accounts')->where('id', $toId)->increment('balance', $amount);
}, 3); // retry up to 3 times on deadlock
// Manual control
DB::beginTransaction();
try {
$invoice = Invoice::create($data);
$payment = $invoice->processPayment();
if (!$payment->isSuccessful()) {
DB::rollBack();
return back()->withErrors('Payment failed');
}
DB::commit();
return redirect()->route('invoices.show', $invoice);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
// afterCommit — defer side effects until AFTER the transaction succeeds
DB::transaction(function() use ($order) {
$order->update(['status' => 'confirmed']);
// This runs ONLY if the transaction commits — safe!
DB::afterCommit(function() use ($order) {
SendOrderConfirmation::dispatch($order->id);
});
});
// Without afterCommit, if the transaction rolls back, the job is already dispatched
// Check current transaction level
DB::transactionLevel(); // 0 = no transaction, 1+ = inside transaction(s)