How to prevent double bookings in a coworking space
A double booking is rarely a calendar problem. It is almost always a definition problem: two people disagreed about what "occupied" means. Here is how to define it precisely enough that the software can enforce it.
The problem is not the calendar
Most coworking spaces discover double bookings the same way: two people arrive for the same meeting room at 10:00, both holding a confirmation. The instinct is to blame the calendar view. But a calendar is a drawing of the data — if two overlapping bookings exist in the database, the calendar will faithfully draw both.
Preventing double bookings means rejecting the second booking at the moment it is created. That requires answering a question most booking tools answer vaguely: what exactly counts as a collision?
Back-to-back bookings are not collisions
The single most common bug in home-grown booking systems is treating 10:00–11:00 and 11:00–12:00 as overlapping. They are not. If your system rejects them, you have just made your meeting rooms unsellable for half the day.
The fix is to treat every reservation as a half-open interval — it includes its start instant and excludes its end instant. Two intervals overlap only when each one starts before the other ends:
start_datetime < :requested_end
AND end_datetime > :requested_start
Read it back with the awkward case. For 10:00–11:00 against a request for 11:00–12:00: 10:00 < 12:00 is true, but 11:00 > 11:00 is false. No overlap, booking accepted. For a request of 10:30–11:30 the second test becomes 11:00 > 10:30, which is true, and the booking is rejected. This is the whole rule, and getting it right removes an entire class of complaints in both directions.
"Occupied" means two different things
A meeting room and a hot desk fail in different ways, and a system that models only one of them will get the other wrong.
- Whole-resource booking. Someone takes the meeting room. Capacity is irrelevant — one booking makes the resource unavailable to everyone else, no matter how few people turn up.
- Per-seat booking. Someone takes three desks in an open area of twelve. The resource stays available until the sum of seats already reserved in that window reaches capacity.
Notice that the second case is not an overlap test at all — it is a sum. You are not asking "does another booking exist?" but "how many people are already booked in this window, and is there room for one more?" In Boris that is a SUM(people_count) over the overlapping bookings, subtracted from the resource's capacity.
The four ways a booking gets rejected
Once both models exist, a request has to survive four separate checks. They are worth naming individually, because "the room is not available" is a useless message to give a member — each of these has a different remedy.
- The resource is blocked. Maintenance, cleaning, a private event. This is not a booking, so it will never appear in a booking-versus-booking overlap test, and it has to be checked separately.
- A whole-resource booking already covers the window. Someone has the room. Nothing else fits, at any size.
- Seats are already sold. Someone wants the whole room, but individual desks in it are already reserved. The room is not free even though no whole-room booking exists.
- Capacity is exhausted. Seats remain, but fewer than the number requested. The useful message here includes the number still available, so the member can book what is left instead of guessing.
Boris returns these as distinct outcomes rather than one generic failure, which is what lets the interface say "only 2 places left in this window" instead of "unavailable".

Cancelled bookings must release the slot
An easy way to reintroduce the problem you just solved: forget to filter by status. A cancelled booking still sits in the table, still overlaps, and will keep blocking a resource that is in fact free. Every availability query has to restrict itself to the states that actually hold the resource — in Boris, pending and confirmed.
This cuts the other way too. If pending does not hold the resource, two people can complete checkout simultaneously for the same slot. A pending booking should reserve the resource and expire on a timer, rather than being ignored until payment lands.
Log the rejection, not just the booking
The bookings you accepted are visible by definition. The ones you rejected are not, and they are the more interesting data: a room that refuses bookings twenty times a week is telling you to buy another room, or to shorten your maximum slot length.
Boris records every blocked conflict against the resource and raises a notification, de-duplicated per resource and time window so that ten refusals for the same slot do not become ten alerts. Over a month that log is the most honest demand signal a space has — it measures what people wanted and could not have, which no occupancy report will ever show you.
A short checklist
- Use half-open intervals, so back-to-back bookings are allowed.
- Model whole-resource and per-seat bookings separately.
- Check resource blocks as their own case, not as bookings.
- Filter availability by status, and let pending hold the slot with an expiry.
- Return the remaining capacity in the error, not just a refusal.
- Keep the rejections — they are your demand data.
