DEV Community

Usama Habib
Usama Habib

Posted on • Originally published at osamahabib.com

How to Prevent Double Bookings in Next.js (Race Conditions & DB Constraints)

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 } });
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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 });
  }
}
Enter fullscreen mode Exit fullscreen mode

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 &&
);
Enter fullscreen mode Exit fullscreen mode

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 } },
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

👉 osamahabib.com — Prevent Double Bookings Next.js

Top comments (0)