Think about what happens when a customer places an order on your e-commerce site. Your code needs to do several things: create the order record, deduct items from inventory, charge the payment, and send a confirmation email.
Now imagine the payment charge succeeds but then your server crashes before the inventory is updated. You have charged the customer but your stock count is wrong. That is a data consistency problem.
Database transactions solve this. Either all the operations succeed together, or none of them do.
The basic idea
A transaction wraps multiple database operations into one unit. If anything goes wrong inside the transaction, everything is rolled back — as if none of it happened.
DB::transaction(function () use ($cart, $user) {
$order = Order::create([
'user_id' => $user->id,
'total' => $cart->total(),
'status' => 'pending',
]);
foreach ($cart->items as $item) {
$order->items()->create([
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'price' => $item->price,
]);
// Deduct from inventory
$item->product->decrement('stock', $item->quantity);
}
$this->paymentService->charge($user, $order->total);
});If $this->paymentService->charge() throws an exception, Laravel automatically rolls back the order creation and the inventory changes. The database is left exactly as it was before.
When you need more control
Sometimes you want to handle the rollback yourself:
DB::beginTransaction();
try {
$order = Order::create($orderData);
$this->processPayment($order);
$this->updateInventory($order);
DB::commit();
return $order;
} catch (PaymentFailedException $e) {
DB::rollBack();
// Tell the user their payment failed
throw $e;
} catch (\Exception $e) {
DB::rollBack();
// Log unexpected errors
Log::error('Order creation failed', ['error' => $e->getMessage()]);
throw $e;
}A common mistake
Transactions only cover database operations. If you send an email inside a transaction and then the transaction rolls back, the email has already been sent. You cannot un-send it.
For this reason, keep side effects like emails and external API calls outside the transaction, or use queued jobs that only run after the transaction commits:
DB::transaction(function () use ($user) {
$order = Order::create(...);
// Queue the email — it will only run after the transaction commits
dispatch(new SendOrderConfirmation($order))->afterCommit();
});The afterCommit() method is a Laravel feature that holds the job until the transaction successfully commits. Very useful.
When to use transactions
Use them whenever you have two or more related database writes that must all succeed or all fail. Creating a user and their profile. Transferring money between accounts. Processing an order. These are all classic transaction scenarios.
The performance cost is minimal. The protection they provide is significant.




