5 min read
DB Transaction in Laravel

When I first built a feature involving multiple database operations at once, I was worried: “If one step fails, will the data become a mess?” After using Database Transactions, that worry disappeared because all processes became safer and more consistent.

Why Do We Need Transactions?

For example, let’s say in a top-up or balance transfer feature:

// Without Transaction - DANGEROUS!
$sender = User::find(1);
$sender->balance -= 100000;
$sender->save();

// Suddenly an error occurs here! Server crash or something...
// Money has been deducted but hasn't reached the receiver

$receiver = User::find(2);
$receiver->balance += 100000;
$receiver->save();

With transactions, if there’s an error anywhere, everything will be rolled back.

Using Transactions in Laravel

Laravel has several methods for using transactions. Here I’ll show examples of what I commonly encounter.

1. Automatic Method (Simple)

This is the simple approach. Laravel will automatically rollback if there’s an exception.

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $sender = User::find(1);
    $sender->balance -= 100000;
    $sender->save();
    
    $receiver = User::find(2);
    $receiver->balance += 100000;
    $receiver->save();
    
    // If there's an error anywhere, automatic rollback!
});

Just wrap database operations in a DB::transaction() closure.

2. Manual Method (More Flexible)

If you need more control, you can use the manual method with beginTransaction, commit, and rollback. This is the writing method I often use because I find it easier to understand.

use Illuminate\Support\Facades\DB;

DB::beginTransaction();

try {
    $sender = User::find(1);
    $sender->balance -= 100000;
    $sender->save();
    
    $receiver = User::find(2);
    $receiver->balance += 100000;
    $receiver->save();
    
    DB::commit(); // If everything is okay, commit
    
} catch (\Exception $e) {
    DB::rollBack(); // If there's an error, rollback
    
    // Handle error
    return response()->json(['error' => $e->getMessage()], 500);
}

3. Return Value from Transaction

You can also return a value from the transaction closure:

$result = DB::transaction(function () {
    $order = Order::create([
        'user_id' => 1,
        'total' => 150000
    ]);
    
    $product = Product::find(10);
    $product->stock -= 1;
    $product->save();
    
    return $order; // Return the newly created order
});

return response()->json($result);

Simple Real-Case Examples

Balance Transfer

public function transfer(Request $request)
{
    $validated = $request->validate([
        'receiver_id' => 'required|exists:users,id',
        'amount' => 'required|numeric|min:1000'
    ]);
    
    DB::transaction(function () use ($validated) {
        $sender = auth()->user();
        $receiver = User::findOrFail($validated['receiver_id']);
        $amount = $validated['amount'];
        
        // Validate sufficient balance
        if ($sender->balance < $amount) {
            throw new \Exception('Insufficient balance');
        }
        
        // Deduct sender's balance
        $sender->balance -= $amount;
        $sender->save();
        
        // Add to receiver's balance
        $receiver->balance += $amount;
        $receiver->save();
        
        // Record history
        Transaction::create([
            'sender_id' => $sender->id,
            'receiver_id' => $receiver->id,
            'amount' => $amount,
            'type' => 'transfer'
        ]);
    });
    
    return response()->json(['message' => 'Transfer successful']);
}

Checkout Order

public function checkout(Request $request)
{
    $order = DB::transaction(function () use ($request) {
        // Create order
        $order = Order::create([
            'user_id' => auth()->id(),
            'total' => $request->total,
            'status' => 'pending'
        ]);
        
        // Loop items
        foreach ($request->items as $item) {
            $product = Product::findOrFail($item['product_id']);
            
            // Check stock
            if ($product->stock < $item['quantity']) {
                throw new \Exception("Stock for {$product->name} is insufficient");
            }
            
            // Reduce stock
            $product->stock -= $item['quantity'];
            $product->save();
            
            // Create order item
            OrderItem::create([
                'order_id' => $order->id,
                'product_id' => $product->id,
                'quantity' => $item['quantity'],
                'price' => $product->price
            ]);
        }
        
        return $order;
    });
    
    return response()->json($order);
}

Delete User with Relations

public function deleteUser($userId)
{
    DB::transaction(function () use ($userId) {
        $user = User::findOrFail($userId);
        
        // Delete all user's posts
        $user->posts()->delete();
        
        // Delete all user's comments
        $user->comments()->delete();
        
        // Delete user
        $user->delete();
    });
    
    return response()->json(['message' => 'User successfully deleted']);
}

Retry Transaction

Laravel also has a retry feature. If a transaction fails due to deadlock, it can automatically retry.

DB::transaction(function () {
    // database operations
}, 5); // Retry 5 times if it fails

Nested Transaction

Laravel supports nested transactions with savepoints. But honestly, avoid nested transactions as much as possible because they make the code complex and difficult to understand.

DB::transaction(function () {
    User::create([...]);
    
    DB::transaction(function () {
        Post::create([...]);
    });
});

When to Use Transactions?

✅ Must Use:

  • Money/balance transfers
  • Checkout/orders with multiple items
  • Updating interdependent data
  • Delete with cascade (delete user + posts + comments)
  • Create with relations (create order + order items)

❌ Not Needed:

  • Single independent insert/update
  • Read-only operations
  • Unrelated operations

Common Mistakes

1. Forgetting to Rollback

// ❌ Bad
DB::beginTransaction();
try {
    // operations
    DB::commit();
} catch (\Exception $e) {
    // Forgot to rollback!
    return response()->json(['error' => $e->getMessage()]);
}

2. Transaction Too Large

// ❌ Bad
DB::transaction(function () {
    // 100 database operations
    // Loop through hundreds of data
    // Heavy processing
});

3. Catch Exception Without Re-throwing

// ❌ Bad
DB::transaction(function () {
    try {
        // operations that can error
    } catch (\Exception $e) {
        // Catch but don't throw again
        // Transaction will still commit!
    }
});

Database transactions in Laravel are simple, but there are several things to remember:

  • Use DB::transaction() for auto-handling
  • Use beginTransaction(), commit(), rollback() if you need more control
  • Validate first before entering a transaction
  • Don’t forget error handling

With proper use of transactions, data operations will be more consistent and safe if any process fails.