Essential cookies keep this site working. With your consent I will also use analytics cookies (visit statistics) and external content (the booking calendar). You can change your decision at any time in the footer.

Cookie policy · Privacy policy

A capacity limit that cannot be exceeded

KMP-002 Snippet 1 min read

Two parents click “Book” in the same second and one place is left. How to prevent overbooking in Laravel with a transaction and a row lock.

The most common bug in booking systems: checking free capacity and saving the booking are two separate queries. A second request can slip in between them. When bookings open for a popular camp session, that is not a theory but a certainty.

The fix: a transaction and lockForUpdate

<?php

use App\Exceptions\CampSessionFullException;
use App\Models\Booking;
use App\Models\CampSession;
use Illuminate\Support\Facades\DB;

function reserve(int $sessionId, array $data): Booking
{
    return DB::transaction(function () use ($sessionId, $data) {
        // Lock the session row until the transaction ends.
        // A second request waits here instead of counting the same places.
        $session = CampSession::whereKey($sessionId)
            ->lockForUpdate()
            ->firstOrFail();

        $taken = $session->bookings()
            ->whereIn('status', ['pending', 'paid'])
            ->count();

        if ($taken >= $session->capacity) {
            throw new CampSessionFullException($session);
        }

        return $session->bookings()->create($data + ['status' => 'pending']);
    }, attempts: 3); // retry on deadlock
}

Why it works

SELECT … FOR UPDATE in InnoDB locks the session row. A second request that wants the same row waits until the first transaction finishes, and only then counts the places, now including the new booking.

What else

  • pending bookings occupy a place too. A separate job cancels unpaid ones after the payment deadline and frees the place.
  • The message for the parent must never read “Error 500”. CampSessionFullException maps to a clear explanation and a waiting-list offer.
  • A limit checked in JavaScript is only a hint for the user. The server tells the truth.