- Booking systems
- Payments
- Database testing
The Booking Bugs That Cost Real Money
Double bookings, time zones, refund windows, and payments that succeed twice. The edge cases I test on every booking flow, and why the database is the only honest witness.
Booking software has a property that most software does not: its defects have an invoice attached. A double-booked slot means someone is turned away at the door. A refund window off by an hour means money leaves that should not have. A payment captured twice means a chargeback and a customer who no longer trusts you.
Having tested a chauffeur booking app and an aviation hangar platform, these are the edge cases I now check on every booking flow, roughly in order of how expensive they are when missed.
Two people, one slot, one second apart
The single most valuable booking test is also the one most likely to be skipped, because it cannot be done by one person clicking one screen.
Take the last remaining slot and submit two bookings for it at genuinely the same moment — two browsers, two accounts, both sitting on the confirmation button. Only one may succeed.
If both succeed, the availability check and the write are not atomic: the system asks "is this free?", gets "yes" twice, and then writes twice. That is a race condition, and it is invisible in sequential testing because sequential testing never asks the question twice at once.
The variants worth running:
- Two users booking the same slot simultaneously
- One user double-clicking the confirm button
- A mobile client that retries after the response is lost but the write already committed
- Booking a slot that is being cancelled at that instant
Then check the database rather than the screens, because both users may well see a success page while only one row exists — or, worse, both rows exist and neither interface mentions it.
Time zones are not a formatting problem
The bug is never that the time displays wrong. The bug is that two parts of the system disagree about what moment "09:00" refers to.
What I test:
- A user in one time zone booking a resource in another. Whose 09:00 is it? Both answers are defensible; the system must pick one and apply it everywhere — confirmation screen, email, calendar file, reminder notification, and the operator's own schedule.
- A booking that crosses midnight in one zone but not the other. Does it appear on the right day in both views?
- The stored value. It should be an unambiguous instant, in UTC or with an offset. A local time string with no zone is a defect waiting for the first customer who travels.
- Daylight saving transitions. Book across the spring-forward boundary and the autumn one. A one-hour booking at the moment the clock goes back covers two distinct instants, and duration arithmetic that assumes otherwise silently produces the wrong end time.
Daylight saving is worth deliberate effort even though it feels obscure, because it fails exactly twice a year, in production, and the tester who tried it in advance is the reason it does not.
The cancellation window is arithmetic nobody checks
"Free cancellation up to 24 hours before departure" is a calculation, and every calculation has boundaries. Test at the boundary, not near it: 24 hours and one minute before, exactly 24 hours before, 23 hours and 59 minutes before.
Then ask which clock the comparison uses. The server's? The user's? The resource's local time? A booking in a different zone can be inside the window on one clock and outside it on another, and whichever the code picks, someone is going to be told a refund is unavailable when they believe it is not.
Also worth checking: whether the window is enforced only in the interface. If the cancel endpoint accepts a request the screen would have refused, the policy is decorative.
Payment and booking are two systems pretending to be one
A booking flow spans your database and a payment provider, and the interesting failures happen between them.
The states to force deliberately:
- Payment succeeds, booking write fails. The customer has been charged for nothing. This must either roll back or raise something a human sees — silence here is money taken for no service.
- Booking is created, payment fails. Is the slot released, or held indefinitely by an unpaid booking that blocks paying customers?
- Payment is pending when the user closes the tab. Does the webhook still complete the booking? Does the slot stay reserved in the meantime, and for how long?
- The webhook arrives twice. Providers retry by design. A second delivery must not create a second booking or a second charge.
- The webhook never arrives. Is there a reconciliation path, or does that booking stay pending forever?
Use the provider's test cards for declines, insufficient funds, and authentication challenges rather than only the card that always succeeds. Most payment defects live in the failure branches, and the failure branches are the ones that were never exercised by hand.
Money that moves twice
Refunds deserve their own pass, because they are the flow with the least test coverage and the most direct financial consequence.
Cancel a paid booking and check the refund amount against the policy, not against what the screen says. Then try to cancel it again. A second cancellation must not issue a second refund. Partial refunds, cancellation fees, and promotional discounts each need their own case — a percentage discount and a fixed-amount discount will take different code paths, and the one that was not fixed last time is the one that is broken now.
Availability is a query, and queries have edges
Availability logic accumulates edge cases faster than any other part of a booking system:
- Back-to-back bookings that share a boundary — does an 09:00–10:00 booking block a 10:00–11:00 one?
- Buffer or turnaround time between bookings, if the business requires it
- Bookings that span midnight, or multiple days
- Blocked-out maintenance periods overlapping a requested slot
- A resource deactivated while a future booking exists against it
- Recurring bookings, if supported, and what happens when one instance in the series is cancelled
Each of these is a boundary condition, and boundary conditions are where availability code is thinnest.
What I check in the database afterwards
None of the above is confirmed from the interface. A booking flow touches several tables, and a partial write shows a success page while leaving the data inconsistent — which is exactly why I verify results in SQL rather than trusting the screen.
After every booking flow, I run the consistency queries:
-- Bookings that overlap on the same resource: should return nothing, ever
SELECT a.id, b.id, a.resource_id
FROM bookings a
JOIN bookings b
ON b.resource_id = a.resource_id
AND b.id <> a.id
AND b.starts_at < a.ends_at
AND b.ends_at > a.starts_at
WHERE a.status = 'confirmed'
AND b.status = 'confirmed';
-- Cancelled bookings that still hold a captured payment SELECT b.id, b.status, p.status, p.amount FROM bookings b JOIN payments p ON p.booking_id = b.id WHERE b.status = 'cancelled' AND p.status = 'captured'; ```
That first query is the one I would keep if I could only keep one. It answers "has this system ever double-booked anything?" against real-shaped data, and it does not care whether anyone thought to test the path that caused it. Run it against staging after a load of concurrent bookings and it either returns nothing — genuinely reassuring — or hands you a defect with its reproduction case attached.
Both queries belong in the regression suite, not in your notes. So does the overlap check for every new resource type someone adds.
Why these are worth your time
Booking defects share three properties that make them unusually costly. They involve money, so they are noticed. They involve a specific customer at a specific moment, so they cannot be quietly fixed later. And most of them only appear under conditions a single tester clicking through a happy path will never create — concurrency, a different time zone, a retried webhook, a boundary exactly on the hour.
That is the argument for testing them deliberately rather than hoping. Related: how I decide what to re-test when re-testing everything is not an option, and the platforms I have done this on.