• Laravel
  • What are transactions in Laravel?

    Database transactions are one of the most important concepts in modern web development, especially when dealing with operations that need to maintain data consistency. In Laravel, transactions provide a powerful way to ensure that multiple database operations either all succeed or all fail together, maintaining the integrity of your data.


    #Understanding Database Transactions

    A database transaction is a sequence of database operations that are treated as a single unit of work. The key principle behind transactions is the ACID properties:

    • Atomicity: All operations in a transaction succeed or fail together
    • Consistency: The database remains in a valid state before and after the transaction
    • Isolation: Concurrent transactions don't interfere with each other
    • Durability: Once committed, changes are permanent

    #Why Use Transactions in Laravel?

    Consider a scenario where you're processing an online order. You need to:

    1. Reduce the product inventory
    2. Create an order record
    3. Charge the customer's payment method
    4. Send a confirmation email

    If any of these steps fail, you want to roll back all previous operations to maintain data consistency. This is where transactions become essential.


    #Laravel Transaction Methods

    Laravel provides several ways to work with transactions through the DB facade:

    #1. Manual Transaction Control

     1use Illuminate\Support\Facades\DB;
     2
     3DB::beginTransaction();
     4
     5try {
     6    // Your database operations
     7    DB::table('products')->where('id', 1)->decrement('stock', 1);
     8    DB::table('orders')->insert(['product_id' => 1, 'user_id' => 123]);
     9    
    10    DB::commit();
    11} catch (\Exception $e) {
    12    DB::rollBack();
    13    throw $e;
    14}
    

    #2. Automatic Transaction Closure

    The most commonly used method is DB::transaction(), which automatically handles commits and rollbacks:

     1use Illuminate\Support\Facades\DB;
     2
     3DB::transaction(function () {
     4    DB::table('products')->where('id', 1)->decrement('stock', 1);
     5    DB::table('orders')->insert(['product_id' => 1, 'user_id' => 123]);
     6});
    

    #3. Eloquent Model Transactions

    You can also use transactions with Eloquent models:

     1use App\Models\Product;
     2use App\Models\Order;
     3use Illuminate\Support\Facades\DB;
     4
     5DB::transaction(function () {
     6    $product = Product::find(1);
     7    $product->decrement('stock');
     8    
     9    Order::create([
    10        'product_id' => $product->id,
    11        'user_id' => auth()->id(),
    12        'amount' => $product->price
    13    ]);
    14});
    

    #Advanced Transaction Features

    #Nested Transactions (Savepoints)

    Laravel supports nested transactions using savepoints:

     1DB::transaction(function () {
     2    // Outer transaction
     3    
     4    DB::transaction(function () {
     5        // Inner transaction (savepoint)
     6        // If this fails, only this inner transaction is rolled back
     7    });
     8});
    

    #Transaction Retry Logic

    You can specify the number of retry attempts for deadlock situations:

     1DB::transaction(function () {
     2    // Your operations
     3}, 5); // Retry up to 5 times
    

    #Connection-Specific Transactions

    For multiple database connections:

     1DB::connection('mysql')->transaction(function () {
     2    // Operations on MySQL connection
     3});
     4
     5DB::connection('postgres')->transaction(function () {
     6    // Operations on PostgreSQL connection
     7});
    

    #Practical Examples

    #E-commerce Order Processing

     1use App\Models\Product;
     2use App\Models\Order;
     3use App\Models\OrderItem;
     4use Illuminate\Support\Facades\DB;
     5
     6public function processOrder(array $items, int $userId)
     7{
     8    return DB::transaction(function () use ($items, $userId) {
     9        $order = Order::create([
    10            'user_id' => $userId,
    11            'status' => 'pending',
    12            'total' => 0
    13        ]);
    14        
    15        $total = 0;
    16        
    17        foreach ($items as $item) {
    18            $product = Product::lockForUpdate()->find($item['product_id']);
    19            
    20            if ($product->stock < $item['quantity']) {
    21                throw new \Exception('Insufficient stock');
    22            }
    23            
    24            $product->decrement('stock', $item['quantity']);
    25            
    26            $orderItem = OrderItem::create([
    27                'order_id' => $order->id,
    28                'product_id' => $product->id,
    29                'quantity' => $item['quantity'],
    30                'price' => $product->price
    31            ]);
    32            
    33            $total += $product->price * $item['quantity'];
    34        }
    35        
    36        $order->update(['total' => $total]);
    37        
    38        return $order;
    39    });
    40}
    

    #Bank Transfer Simulation

     1use App\Models\Account;
     2use Illuminate\Support\Facades\DB;
     3
     4public function transferMoney(int $fromAccountId, int $toAccountId, float $amount)
     5{
     6    return DB::transaction(function () use ($fromAccountId, $toAccountId, $amount) {
     7        $fromAccount = Account::lockForUpdate()->find($fromAccountId);
     8        $toAccount = Account::lockForUpdate()->find($toAccountId);
     9        
    10        if ($fromAccount->balance < $amount) {
    11            throw new \Exception('Insufficient funds');
    12        }
    13        
    14        $fromAccount->decrement('balance', $amount);
    15        $toAccount->increment('balance', $amount);
    16        
    17        // Log the transaction
    18        TransactionLog::create([
    19            'from_account_id' => $fromAccountId,
    20            'to_account_id' => $toAccountId,
    21            'amount' => $amount,
    22            'timestamp' => now()
    23        ]);
    24        
    25        return true;
    26    });
    27}
    

    #Best Practices

    #1. Keep Transactions Short

    Minimize the time transactions are held open to avoid blocking other operations:

     1// Good: Quick transaction
     2DB::transaction(function () {
     3    $user = User::create($userData);
     4    $profile = Profile::create(['user_id' => $user->id] + $profileData);
     5});
     6
     7// Avoid: Long-running operations in transactions
     8DB::transaction(function () {
     9    // ... database operations ...
    10    sleep(30); // This blocks other transactions
    11    // ... more operations ...
    12});
    

    #2. Use Pessimistic Locking When Needed

    For critical operations, use lockForUpdate() to prevent race conditions:

     1DB::transaction(function () {
     2    $account = Account::where('id', $accountId)->lockForUpdate()->first();
     3    
     4    if ($account->balance >= $withdrawAmount) {
     5        $account->decrement('balance', $withdrawAmount);
     6    }
     7});
    

    #3. Handle Exceptions Properly

    Always handle potential exceptions in your transaction code:

     1try {
     2    $result = DB::transaction(function () {
     3        // Your operations
     4        return $someResult;
     5    });
     6    
     7    return response()->json(['success' => true, 'data' => $result]);
     8} catch (\Exception $e) {
     9    Log::error('Transaction failed: ' . $e->getMessage());
    10    return response()->json(['success' => false, 'message' => 'Operation failed']);
    11}
    

    #4. Avoid External API Calls in Transactions

    Don't make HTTP requests or external API calls within transactions:

     1// Wrong: External API call in transaction
     2DB::transaction(function () {
     3    $order = Order::create($orderData);
     4    Http::post('https://api.payment.com/charge', $paymentData); // Don't do this
     5});
     6
     7// Better: Separate concerns
     8$order = DB::transaction(function () {
     9    return Order::create($orderData);
    10});
    11
    12// Then make external calls
    13try {
    14    $paymentResult = Http::post('https://api.payment.com/charge', $paymentData);
    15    $order->update(['payment_status' => 'completed']);
    16} catch (\Exception $e) {
    17    $order->update(['payment_status' => 'failed']);
    18}
    

    #Testing Transactions

    Laravel provides helpful methods for testing code that uses transactions:

     1use Illuminate\Foundation\Testing\RefreshDatabase;
     2
     3class OrderTest extends TestCase
     4{
     5    use RefreshDatabase;
     6    
     7    public function test_order_creation_with_insufficient_stock()
     8    {
     9        $product = Product::factory()->create(['stock' => 1]);
    10        
    11        $this->expectException(\Exception::class);
    12        
    13        DB::transaction(function () use ($product) {
    14            $orderService = new \App\Services\OrderService;
    15            
    16            // Try to order more than available stock
    17            $orderService->createOrder([
    18                ['product_id' => $product->id, 'quantity' => 5]
    19            ]);
    20        });
    21        
    22        // Verify the product stock wasn't changed due to rollback
    23        $this->assertEquals(1, $product->fresh()->stock);
    24    }
    25}
    

    #Common Pitfalls to Avoid

    #1. Forgetting to Handle Rollbacks

    Always ensure proper error handling and rollbacks.

    #2. Long-Running Transactions

    Keep transactions as short as possible to avoid deadlocks.

    #3. Mixing Transaction Types

    Don't mix manual transaction control with automatic closures.

    #4. Ignoring Connection Context

    Be aware of which database connection your transaction is running on.


    #Conclusion

    Laravel transactions are a powerful tool for maintaining data integrity in your applications. By understanding when and how to use them effectively, you can build robust applications that handle complex operations safely. Remember to keep transactions short, handle exceptions properly, and always consider the performance implications of your transaction design.

    Whether you're building an e-commerce platform, financial application, or any system that requires data consistency, mastering Laravel transactions is essential for creating reliable and maintainable code.

    profile image of Petar Vasilev

    Petar Vasilev

    Petar is a web developer at Mitkov Systems GmbH. He is fascinated with the web. Works with Laravel and its ecosystem. Loves learning new stuff.

    More posts from Petar Vasilev