---
title: Wrap Multi-Step Database Operations in Transactions
impact: HIGH
impactDescription: partial failures leave the database in an inconsistent state without transactions
tags: transactions, database, atomicity, eloquent, laravel
---

## Wrap Multi-Step Database Operations in Transactions

Any sequence of writes that must succeed or fail together (e.g. create order + deduct stock + charge payment) must run inside a `DB::transaction()`.

**Wrong:**

```php
// If deductStock() succeeds but createTransaction() fails,
// stock is deducted but no order exists — corrupted state
public function placeOrder(Cart $cart, User $user): Order
{
    $order = Order::create([...]);
    $this->deductStock($cart->items);    // can throw
    $this->createTransaction($order);    // can throw — order exists, stock reduced
    $this->clearCart($cart);
    return $order;
}
```

**Correct:**

```php
use Illuminate\Support\Facades\DB;

public function placeOrder(Cart $cart, User $user): Order
{
    return DB::transaction(function () use ($cart, $user) {
        $order = Order::create([
            'user_id' => $user->id,
            'total'   => $cart->total(),
        ]);

        $this->deductStock($cart->items);    // exception -> full rollback
        $this->createTransaction($order);    // exception -> full rollback
        $this->clearCart($cart);

        return $order;
    });
    // Any Throwable inside auto-rolls back and re-throws
}
```

**With custom exception handling:**

```php
DB::transaction(function () {
    // ...
}, attempts: 3); // retry on deadlock (3 attempts)
```

**Manual control when needed:**
```php
DB::beginTransaction();
try {
    // ...
    DB::commit();
} catch (Throwable $e) {
    DB::rollBack();
    throw $e;
}
```

**Note:** Dispatching jobs inside a transaction should use `afterCommit()`:
```php
ProcessOrder::dispatch($order)->afterCommit(); // only dispatches if transaction commits
```
