# How do you stop double bookings?

> Two people pick the same time in the same second, and a check in code passes for both. A database rule refusing overlaps is the one check that cannot race.

- One person cannot be booked twice: the database refuses the second.
- Buffers are inside the rule, so nothing is booked into a clean-down.
- An unpaid booking holds its slot for 15 minutes, unless you change it.

## Why checking first is not enough

Most booking code does two things in turn: it asks whether the time is free,
and if it is, it saves the booking. Between the question and the save there is
a gap a few milliseconds long. Two customers who press book at the same moment
both ask, both hear that ten o'clock is free, and both save. Neither did
anything wrong, and the diary now holds two people at ten.

It happens rarely, which is what makes it hard to catch: never in testing,
once on a busy Saturday. Making every booking wait in line behind the others
closes the gap, at the cost of slowing exactly the moments that are busiest.

## The check that cannot race

Let the database refuse it. Postgres can hold a rule that no two rows for the
same person may have overlapping times, called an exclusion constraint, and it
applies the rule as it writes, to every write. Two saves arriving together
cannot both pass it: one is written and the other is refused, however close
together they came.

BackendFree's bookings sit under exactly that rule:

- **Per member of staff.** Two people can each be booked at ten o'clock. One
  person cannot be booked twice.
- **Over the whole blocked time.** A service's buffers are part of the time it
  takes out of the diary, so nothing can start inside somebody else's
  clean-down.
- **Only while a booking holds its slot.** Waiting for payment, waiting for the
  business, or confirmed. A cancelled or expired booking gives its time back
  the moment its status changes.
- **Touching is not overlapping.** One appointment ending at 10:00 and the next
  starting at 10:00 both fit.

## What the customer who lost sees

The second booking is refused with `slot_unavailable`, the same answer as a
time that was never free, and nothing of it is left half written. Ask for the
free times again and offer the next one.

The time asked for is checked again when the booking is made, rather than
trusted from the page that offered it, so a time outside opening hours, inside
the notice period or off the grid is refused the same way.

## Holds while somebody pays

A booking that has to be paid for holds its slot while the customer pays,
15 minutes unless you change it,
and nobody else can take that time meanwhile. A hold that has run out is
written off before the next booking for the same time is tried, so nobody is
refused a slot that nobody is holding.

If the money arrives after the hold ran out, the booking comes back only if
the time is still free, decided by the same rule. If somebody else has taken
it since, the booking is marked as owing a refund instead.

## A double click is not a second booking

A booking form is often sent twice: a double click, or a retry after a slow
network. Each request to book carries a key of its own, and the same key always
comes back as the same booking. The SDK adds one to every write, so retrying
`book()` is safe. Booking again to get a new payment page is not: the first
booking is already holding the slot.

[Bookings](/product/bookings/) has the rest of the diary, and [How do
you leave time between appointments?](/guides/time-between-appointments/)
covers the buffers.

## Questions people ask next

### Can two people on the staff be booked at the same time?

**Yes.** The rule is per member of staff. Two stylists at ten o'clock are two appointments, and one stylist booked twice at ten is refused.

### Can two customers pay for the same slot?

**No.** A booking waiting for payment holds its slot against everybody else, so the second customer is refused before they are ever sent to pay.

### Does a cancelled booking free its time straight away?

**Yes.** A cancelled or expired booking stops holding its slot the moment its status changes, and the time is offered again.

### What does my code have to do?

**Handle the refusal.** When a booking comes back as slot_unavailable, ask for the free times again and offer the next one. Nothing else has to check.

## Sources

- [Constraints: exclusion constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION), PostgreSQL
