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.