Laravel Booking Forms: Prevent Double Bookings With Database Locks

Laravel
Booking Systems
MySQL
Database Transactions
Concurrency
Custom Web Applications
Payment Holds

By Kashii Malik ? Updated

A practical Laravel design for limited booking capacity: lock the slot, allocate atomically, manage payment holds, and test competing requests.

Laravel Booking Forms: Prevent Double Bookings With Database Locks

There is one place left in a session. Two customers open the booking form, both see availability, and both click Reserve. Each request checks the same remaining count before either request saves its booking. Both pass. The venue now has a problem that a nicer calendar cannot fix.

This is a concurrency problem: two valid requests overlap in time. In a Laravel application, the important protection belongs around the database decision that allocates capacity. Here is a practical design for fixed time slots with a limited number of places.

Why checking availability first is not enough

An availability response describes what was available when it was calculated. It is useful for the interface, but it cannot promise that a place will still exist when the customer submits.

Disabling the button can reduce accidental clicks in one browser. It cannot coordinate another customer, a receptionist using an admin screen, or an API request from a mobile app. All those booking paths need to use the same allocation rule.

Start by writing the rule plainly: confirmed places plus capacity-consuming holds must never exceed the slot’s capacity. Decide whether a reservation is a whole room, one seat, or a quantity of admissions. Those are different models.

Give each bookable slot a stable database row

For fixed sessions, I would create a slot record in advance with its venue, start time, capacity, and allocated count. Reservations reference that slot and record a positive quantity, status, and any hold expiry.

That existing slot row becomes the point where competing writers coordinate. Avoid relying on a query that finds no matching reservation and assuming there is now a row lock protecting the empty space. Lock behavior around missing rows and ranges depends on the database and isolation level.

This guide assumes MySQL InnoDB. Its locking reads documentation explains that a FOR UPDATE read acquires locks held until the transaction commits or rolls back. Other databases need their own verified locking behavior.

Lock, check, allocate, and save in one transaction

Laravel exposes lockForUpdate() through the query builder. Its pessimistic locking documentation recommends using locks within a transaction. The lock should cover the decision and the database writes that depend on it.

A reservation service can follow this sequence:

  • Validate a positive integer quantity and authorize the customer’s access to the requested slot or business account.
  • Begin a database transaction and read the existing slot row by its indexed identifier using lockForUpdate().
  • Check that the slot is open, its booking deadline has not passed, and capacity minus allocated places can cover the requested quantity.
  • If it cannot, return an unavailable result without creating a reservation.
  • If it can, increase the allocated count and create the reservation or temporary hold inside the same transaction.
  • Commit before returning a successful reservation result.

When the next cooperating request acquires that slot lock, it checks the updated count. The protection works only if every writer follows this protocol. An admin shortcut that inserts a booking without allocating capacity can still break the rule.

Keep the counter update and reservation insert on the same transactional database connection. If either write fails, roll back both. Include cancellations, reschedules, and capacity edits in the design rather than treating them as unrelated CRUD screens.

A slot key is not a general overlap detector

A unique key can prevent two slot records being created for the same resource and start time. Laravel’s migration index documentation covers database unique indexes. These are useful backstops, but they enforce the particular key you define.

They do not automatically prevent a 10:00–11:00 room booking from overlapping a 10:30–11:30 booking. For arbitrary intervals, choose a separate strategy: for example, serialize all booking changes through an existing resource row, then perform the overlap check inside that transaction. Every competing write must lock that same resource first.

For a first implementation, fixed slots are often easier to explain and test. Use them when they match the business’s real scheduling rules, rather than forcing variable appointments into an unsuitable data model.

AI-generated photorealistic editorial scene of a small meeting room and booking clipboard. Conceptual image illustrating limited booking capacity; not a real client location or evidence of a reservation.

AI-generated photorealistic editorial scene of a small meeting room and booking clipboard. Conceptual image illustrating limited booking capacity; not a real client location or evidence of a reservation.

Conceptual AI-generated editorial image illustrating a shared room with limited booking availability. It is not a real client location or evidence of a reservation.

Hold places during payment without holding a database lock

Do not leave a database transaction open while a customer enters card details or while an external payment request runs. Allocate a temporary hold, save its expiry, and commit. The payment process can then refer to that reservation.

Use explicit states such as held, confirmed, expired, and cancelled. In a counter-based design, a hold consumes capacity until an atomic state transition releases it. Merely passing the expiry timestamp does not decrement the counter.

The expiry worker and payment confirmation handler should lock the same slot and reservation in a consistent order, recheck the current state, and apply only a valid transition. Confirmation converts a valid hold into a confirmed reservation without allocating the places twice. Expiry releases them once.

If confirmation arrives after the hold was released, do not blindly mark the booking confirmed. Recheck capacity through the allocation service or route the case to a documented recovery process. Payment success and reservation availability are separate facts.

Handle repeat submissions separately from competing customers

A capacity lock stops overselling the last place. It does not stop one customer accidentally purchasing two different places when plenty are available.

For retry-safe submission, store an operation key with the reservation and enforce uniqueness within the appropriate customer or account scope. Bind it to the submitted slot and quantity. A repeated key with the same payload should return the existing operation; a different payload should be rejected.

Keep that idempotency decision atomic with the allocation. If a uniqueness conflict causes a transaction to fail, its capacity change must roll back before the existing operation is returned. Do not trust the key as proof of authorization.

Keep transactions short and plan for retries

Laravel’s database transaction documentation describes automatic rollback on exceptions and a bounded retry option for deadlocks. A retried transaction may run its callback again.

That means an email, payment request, or other external effect inside the callback could be repeated. Commit durable booking changes first. For reliable notifications, consider writing an outbox record in the transaction and processing it separately.

When a booking touches several slots, such as a reschedule, acquire their locks in a consistent order. Keep reads targeted by indexes. Monitor lock waits and deadlocks, and show a clear retryable error if the database cannot complete the operation.

Test requests that actually overlap

  • Create a slot with one available place and submit two independent requests at the same time. Exactly one should allocate it.
  • Request quantities that together exceed capacity. Check the final counter and reservation rows, not just the HTTP responses.
  • Force the reservation insert to fail after the counter update. Confirm both writes roll back.
  • Retry the same operation key and verify there is one allocation. Reuse it with different input and verify rejection.
  • Race hold expiry against payment confirmation. Confirm capacity is released or retained once, according to the final state.
  • Run the same tests through admin bookings, cancellations, and reschedules.

Use separate database connections or processes and the same database engine and isolation settings as production. A sequential test or an in-memory database does not demonstrate that the locking protocol works under contention.

The customer-facing result should be simple: a confirmed reservation, an explicit temporary hold, or a clear message that availability changed. The database work underneath makes those messages dependable.

For related booking development, see my Jump World online ticketing project. The locking design here is illustrative, not a claim about that project’s internal implementation. If your booking flow needs dependable capacity checks across web and admin requests, I’m happy to discuss it.

Primary documentation checked on 4 November 2026. Verify locking behavior against the database engine, isolation level, and Laravel version used by your application.

Share this post:

Related Posts

REST API Rate Limits: Handle 429 Errors Without Losing Sync Progress
October 31, 2026

REST API Rate Limits: Handle 429 Errors Without Losing Sync Progress

A practical guide to handling API rate limits with sensible retry delays, shared request budgets, resumable syncs, and clear failure states in Laravel integrations.

Laravel Scheduled Tasks: Prevent Duplicate Runs and Silent Failures
October 29, 2026

Laravel Scheduled Tasks: Prevent Duplicate Runs and Silent Failures

A practical Laravel scheduler checklist for client portals: verify cron, prevent duplicate runs, check queue workers and catch reports or reminders that never finish.

Secure File Uploads in Laravel: A Practical Client Portal Checklist
October 26, 2026

Secure File Uploads in Laravel: A Practical Client Portal Checklist

A practical guide to Laravel document uploads: validate files, keep storage private, control downloads, handle failed checks, and test the complete client portal workflow.