HelpfulMods

Booking & Calendar

Two customers chose the same appointment. Let the database settle it.

Two people can see the same available appointment before either presses Book. If your server checks availability and then inserts a reservation as separate operations, both requests can pass the check.

Removing the slot from the first person’s browser will not fix that race. The decision has to survive two requests reaching the backend together, including requests handled by different processes.

Two simultaneous requests share one capacity guard and one available slot; one is held while the other receives a conflict and refreshes.
Both requests must use the same database capacity rule. One hold wins; the other refreshes availability.

Reserve the interval in one database operation

For a single-capacity resource, PostgreSQL can reject overlapping reservations with an exclusion constraint. A range expresses the occupied interval; the constraint combines that interval with the resource ID. PostgreSQL documents this pattern for room reservations in its range type guide.

Here is a small schema you can run in a disposable PostgreSQL database. Your database administrator must permit the btree_gist extension:

CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE appointment_holds (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  resource_id text NOT NULL,
  starts_at timestamptz NOT NULL,
  ends_at timestamptz NOT NULL,
  status text NOT NULL CHECK (status IN ('held','confirmed','expired','cancelled')),
  expires_at timestamptz,
  CHECK (ends_at > starts_at),
  CHECK (status <> 'held' OR expires_at IS NOT NULL),
  EXCLUDE USING gist (
    resource_id WITH =,
    tstzrange(starts_at, ends_at, '[)') WITH &&
  ) WHERE (status IN ('held','confirmed'))
);

If two transactions insert the same resource and interval, one can commit and the other receives an exclusion violation, SQLSTATE 23P01. Catch that specific error and return a conflict response that refreshes availability. Keep other database errors visible so a broken connection does not masquerade as a fully booked calendar.

The half-open interval permits one appointment to begin when another ends. If you need setup or cleanup time, include that buffer in the blocked interval. Keep the displayed appointment time separate from the longer interval you reserve.

Give holds a real expiry transition

The passage of time does not change a row’s status. An expired timestamp with status='held' still blocks the interval under this constraint.

Run an expiry job, and expire elapsed holds for the relevant resource inside the booking transaction before trying a new insert. Use a consistent lock for that resource when several operations must agree. PostgreSQL provides transaction-level advisory locks for application-defined coordination; they release at transaction end. Its locking guide explains their behavior.

Keep the exclusion constraint as the final database rule even when the application takes a lock. Every path that creates, moves or restores a reservation must obey the same capacity rule.

Keep time zones and payments attached to the hold

Convert the chosen time to an unambiguous instant before inserting it. Store the customer’s display zone separately, and reject a local time that does not exist during a daylight-saving transition. A time-zone conversion library can help translate the display; it cannot decide which of two simultaneous customers owns a slot.

If a deposit is required, attach the provider checkout to the original hold. After verified payment, confirm that hold in a transaction while it still owns the interval. If it expired and another customer now holds the slot, keep the late payment in a state that needs attention. Resolve it through an available replacement or the appropriate refund process. Recreating the old booking over the new reservation would turn a payment delay into a double booking.

An external calendar introduces another limit: your database transaction cannot lock an independent edit in Google Calendar. Decide how fresh the calendar’s busy snapshot must be before offering or holding a slot, and stop selling availability when that required snapshot is unavailable.

Test two requests together

Send two concurrent holds for one resource and interval. Expect one success, one conflict and one live reservation. Then test a different resource, adjacent intervals, expired holds, a competing reschedule and a late deposit. Check the persisted reservations after each test rather than relying only on the HTTP responses.

Booking & Calendar includes serialized local holds, expiry, calendar synchronization and deposit reconciliation. The manual database example above is one way to build capacity protection yourself; the mod’s source connects that protection to the surrounding booking workflow.