Table Of Contents
The Data Corruption Problem
Picture this: You're processing a payment, updating inventory, and sending emails. Halfway through, something breaks. Now you have charged the customer but haven't updated the inventory. Nightmare scenario avoided with transactions:
use Illuminate\Support\Facades\DB;
// RISKY - Operations can fail independently
public function processOrder($orderId)
{
$order = Order::find($orderId);
$order->update(['status' => 'processing']); // ✅ Succeeds
$this->updateInventory($order->items); // ❌ Fails here
$this->chargeCustomer($order->total); // Never runs
$this->sendConfirmationEmail($order->customer); // Never runs
// Order is marked as processing but nothing else happened!
}
// SAFE - All operations succeed or all fail
public function processOrder($orderId)
{
DB::transaction(function () use ($orderId) {
$order = Order::find($orderId);
$order->update(['status' => 'processing']);
$this->updateInventory($order->items);
$this->chargeCustomer($order->total);
$this->sendConfirmationEmail($order->customer);
});
}
Laravel Transaction Patterns
Different ways to handle transactions based on your error handling needs:
// Basic transaction - automatic rollback on exceptions
DB::transaction(function () {
User::create(['name' => 'John', 'email' => 'john@example.com']);
Profile::create(['user_id' => 1, 'bio' => 'Developer']);
// If Profile creation fails, User creation is rolled back
});
// Manual transaction control
DB::beginTransaction();
try {
$user = User::create(['name' => 'Jane', 'email' => 'jane@example.com']);
$profile = Profile::create(['user_id' => $user->id, 'bio' => 'Designer']);
// Some complex business logic
if ($someCondition) {
throw new \Exception('Business rule violation');
}
DB::commit();
return $user;
} catch (\Exception $e) {
DB::rollBack();
Log::error('User creation failed: ' . $e->getMessage());
throw $e;
}
// Transaction with return value
$user = DB::transaction(function () {
$user = User::create(['name' => 'Bob', 'email' => 'bob@example.com']);
// Attach default role
$user->roles()->attach(Role::where('name', 'user')->first());
return $user; // This value is returned from DB::transaction()
});
// Transaction with retry attempts (Laravel 8+)
DB::transaction(function () {
// Your database operations
}, 3); // Retry up to 3 times on deadlock
Advanced Transaction Scenarios
Real-world examples where transactions prevent data inconsistency:
// E-commerce order processing
public function createOrder(array $items, User $customer)
{
return DB::transaction(function () use ($items, $customer) {
// Create order
$order = Order::create([
'user_id' => $customer->id,
'total' => 0,
'status' => 'pending'
]);
$total = 0;
foreach ($items as $item) {
// Check inventory
$product = Product::lockForUpdate()->find($item['product_id']);
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException();
}
// Reduce inventory
$product->decrement('stock', $item['quantity']);
// Add order item
$order->items()->create([
'product_id' => $product->id,
'quantity' => $item['quantity'],
'price' => $product->price
]);
$total += $product->price * $item['quantity'];
}
// Update order total
$order->update(['total' => $total]);
return $order;
});
}
// Bank transfer simulation
public function transferMoney($fromAccountId, $toAccountId, $amount)
{
DB::transaction(function () use ($fromAccountId, $toAccountId, $amount) {
// Lock accounts for update to prevent race conditions
$fromAccount = Account::lockForUpdate()->find($fromAccountId);
$toAccount = Account::lockForUpdate()->find($toAccountId);
if ($fromAccount->balance < $amount) {
throw new InsufficientFundsException();
}
// Debit from source
$fromAccount->decrement('balance', $amount);
// Credit to destination
$toAccount->increment('balance', $amount);
// Log transaction
Transaction::create([
'from_account_id' => $fromAccountId,
'to_account_id' => $toAccountId,
'amount' => $amount,
'type' => 'transfer'
]);
});
}
// Nested transactions (savepoints)
DB::transaction(function () {
User::create(['name' => 'Parent Transaction']);
DB::transaction(function () {
Profile::create(['bio' => 'Nested Transaction']);
// This creates a savepoint, not a new transaction
});
});
The DB::transaction()
method ensures atomicity - either all operations succeed, or none do. Use lockForUpdate()
within transactions to prevent race conditions on critical data like inventory or account balances.
Related: Laravel Collections: Beyond Basic Array Operations | Database Design: Fundamentals of Good Database Architecture | Database Design Patterns: Complete Guide 2025 | Database Design Patterns: Complete Guide 2025 | Database Design: Fundamentals of Good Database Architecture
Add Comment
No comments yet. Be the first to comment!