Most booking tutorials show you how to build a pretty calendar and save a reservation. Then they skip the one problem that actually matters in production.
What happens when two people book the same slot at the same time?
👉 Full guide here
Why "check then save" is a trap
The obvious approach:
const existing = await prisma.booking.findFirst({
where: { resourceId, startTime },
});
if (existing) throw new Error("Slot taken");
await prisma.booking.create({ data: { resourceId, startTime } });
This works - until two requests arrive at the same moment. Both pass the check. Both create a booking. Race condition. Invisible in testing, guaranteed in production.
The fix isn't better app code — it's letting the database enforce the rule atomically.
Fix 1: Unique constraint (fixed slots)
model Booking {
@@unique([resourceId, startTime])
}
Try to insert and catch the error:
try {
await prisma.booking.create({ data: { resourceId, startTime } });
} catch (e) {
if (e.code === "P2002") {
return Response.json({ error: "Slot just taken" }, { status: 409 });
}
}
Whoever commits first wins. Second request gets a clean 409. No race condition — uniqueness check and insert are one atomic op.
Fix 2: Overlapping bookings (the hard case)
Unique constraints only catch identical start times. A 60-min booking from 2:00 can still conflict with one from 2:30.
PostgreSQL exclusion constraint handles this:
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE "Booking"
ADD CONSTRAINT no_overlapping_bookings
EXCLUDE USING gist (
resource_id WITH =,
tstzrange(start_time, end_time) WITH &&
);
Database now rejects any booking whose time range overlaps an existing one — no matter how many concurrent requests hit it.
Fix 3: Transactions for multi-step bookings
await prisma.$transaction(async (tx) => {
await tx.booking.create({ data: { resourceId, startTime, endTime } });
await tx.credit.update({
where: { userId },
data: { balance: { decrement: 1 } },
});
});
Constraint prevents double booking. Transaction keeps related changes consistent.
Don't forget timezones
Always store timestamps in UTC. Convert to local time only for display. "2:00 PM" without timezone is meaningless for overseas customers.
Full guide with FAQ on Redis, queues, and when constraints are enough:
Top comments (0)