Navigation

Laravel

How to Use `DB::transaction()` for Safe Database Operations

Protect Laravel database operations in 2025 using DB::transaction() for atomic queries. Prevent data corruption, handle rollbacks, and ensure ACID compliance.

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

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel