This is not a specific business. We constructed the shape exactly as it works in a small place that takes bookings — a salon, a study room, a shared kitchen. The names and times are invented; the code, screens and logs actually ran.
The worst failure in a booking tool is not that the booking fails.
It is sending "you're booked" to both people. If it fails you try again; if both are told yes, what comes next is somebody sorting it out on the phone.
Two handlers, the same description
This sample has two handlers that take a slot. Described in words they are identical — look whether it is free, and if it is, take it.
One is this.
// 1. Look.
final free = slot.takenBy == null;
// 2. Anything at all in here — a database round trip, a payment check, a
// log write, a lookup of the customer's name — hands control to
// whatever else was in flight.
await Future<void>.delayed(Duration.zero);
// 3. Write, using what was true in step 1.
if (free) { slot.takenBy = who; ... }
The other is this.
if (slot.takenBy != null) {
return '$at is already taken by ${slot.takenBy}';
}
slot.takenBy = who;
slot.confirmations.add(who);
await Future<void>.delayed(Duration.zero); // the slow part, after the claim
The same await, in a different position. In the first it sits between the look and the write; in the second it sits after the write.
And that position produces the double booking.
Make it actually happen
unsafe -> Suh: "CONFIRMED 10:30 for Suh"
unsafe -> Park: "CONFIRMED 10:30 for Park"
unsafe result: 1 slot(s) confirmed to more than one person

The screen shows exactly the real-world symptom. The slot has one owner, Park, and two people were told they had it.
This is the shape it takes in a real shop. The book says one; two turn up at the door.
So the server remembers everybody it confirmed to.
/// Everybody who has ever been told they got this slot. In a correct
/// booking system this never has more than one name in it, which is exactly
/// why it is worth keeping.
final confirmations = <String>[];
Correct means always a one-name list. A list that must always have one entry is exactly the list worth counting.
And then make it not happen
Same two people, same slot, only the gap closed.
--- cleared, same two people, same slot, gap closed ---
safe -> Suh: "CONFIRMED 10:30 for Suh"
safe -> Park: "10:30 is already taken by Suh"
safe result: nobody was told yes twice

The refusal is not "that failed" but "Suh has it". What the second person needs to know is not that they failed but that the slot is gone, so that they pick another time.
Check only the good case and it passes
This sample's verification is unusual. It checks that the bug happened.
grep -q 'unsafe -> Park: "CONFIRMED 10:30 for Park"' captures/run.log \
|| { echo " the race did not happen — this run proves nothing"; exit 1; }
The reason is simple. If a booking test only checks "exactly one was confirmed", it passes against a server with the bug still in it. If the race does not actually happen, the requests are handled in order and the result looks fine.
So the order is this.
- With the unsafe one, confirm the race actually happened
- With the safe one, confirm it does not happen in the same race
Without the first, the second means nothing.
There is one more. If the gap disappears, it fails.
# The two handlers must stay near-identical apart from where the await sits.
# If somebody "fixes" the unsafe one, this sample stops teaching anything.
grep -q "await Future<void>.delayed(Duration.zero);" booking_server/bin/server.dart \
|| { echo " the gap in the unsafe handler is gone — nothing is demonstrated"; exit 1; }
This content requires Developer or above
Sign in and upgrade your plan to continue reading.
View Plans