Laravel cookbook · Intermediate
Prevent race conditions with transactions and row locks
Two requests redeem the same voucher or oversell the same stock.
Approach
Wrap the read-check-write in DB::transaction() and lock the rows with lockForUpdate(), or use a single
conditional update. Pass an attempt count so deadlocks are retried.
Solution
<?php
namespace App\Actions;
use App\Exceptions\VoucherUnavailable;
use App\Models\User;
use App\Models\Voucher;
use Illuminate\Support\Facades\DB;
class RedeemVoucher
{
public function __invoke(User $user, string $code): Voucher
{
return DB::transaction(function () use ($user, $code) {
$voucher = Voucher::where('code', $code)->lockForUpdate()->firstOrFail();
if ($voucher->redeemed_at !== null || $voucher->expires_at->isPast()) {
throw new VoucherUnavailable($code);
}
$voucher->update(['redeemed_at' => now(), 'redeemed_by' => $user->id]);
$user->increment('credit_cents', $voucher->value_cents);
return $voucher;
}, attempts: 3);
}
}
// Single-statement alternative: only one request can match the WHERE clause.
$updated = DB::table('products')
->where('id', $productId)
->where('stock', '>=', $quantity)
->decrement('stock', $quantity);
Pitfalls
- Checking a value, then updating it in a separate query without a lock.
- Sending emails or calling APIs inside the transaction; dispatch them after commit.
- Locking rows in different orders in different code paths, causing deadlocks.
Variations
Cache::lock('voucher:'.$code, 10)->block(5, fn () => ...)serialises work across servers without row locks.- Add unique indexes (one redemption per user) as a last line of defence.
Further reading: Pessimistic locking
Reference: https://laravel.com/docs/13.x/queries#pessimistic-locking
More Laravel recipes
- Authorize actions with policies
- Cache expensive data with stale-while-revalidate
- Call external APIs with timeouts, retries and fakes
- Catch N+1 queries with strict Eloquent mode
- Delete old records automatically with prunable models
- Dispatch events and listeners after the transaction commits
- Issue scoped API tokens with Sanctum
- Make queued jobs reliable with retries, backoff and uniqueness