Part 8 of 18 · 5 min read
Booking under concurrency and time zones
Slot ownership, race conditions, daylight-saving ambiguity, holds and rescheduling.
Arunank Sharan · Kuzushi Labs ·
In this article
An availability result is a snapshot. Between showing it and receiving confirmation, another coordinator may book the same slot. The agent's job is to explain that conflict clearly; the scheduling system's job is to prevent the second booking.
Our appointment service contains availability and slot-selection operations, including a conversation-oriented slot reply path. The registry-to-service mismatch described earlier needs fixing before those operations form a complete agent flow. Here I work through the reference booking implementation that makes concurrency, time and resource versions explicit.
Decide what a slot means
For this chapter, a slot is a single-capacity interval owned by one scheduling system. It already identifies its practitioner, location, start, end and appointment-type constraints. The assistant chooses among server-issued slot IDs; it does not construct a new slot from free text.
That restriction simplifies the commit. A unique active reservation per slot can protect capacity. For a group session, I would instead lock a capacity record and atomically increment reserved capacity subject to its limit. For arbitrary overlapping intervals, I would use an interval-aware reservation model. These cases require different constraints.
Normalize time without guessing
“Tomorrow afternoon” combines a relative date, local time zone and a loose interval. I would resolve the date using the clinic's configured zone, ask for missing constraints and show the resolved date in the preview. The user should see “1 October, 2:30 PM, Asia/Kolkata,” not an unexplained UTC string.
from prm_lab import local_to_utc
start = local_to_utc(
"2026-10-01T14:30:00",
"Asia/Kolkata",
)
assert start == "2026-10-01T09:00:00Z"
The lab also checks daylight-saving boundaries. A local clock time may not exist during the spring transition or may occur twice during the autumn transition. Its helper round-trips both possible folds through UTC, rejects nonexistent times and requires a choice for ambiguous ones. Even if the first clinic operates in India or Singapore, the connector should not quietly assume that every location has a fixed UTC offset.
I would store UTC instants for ordering, plus the clinic's IANA time zone and the original local choice for explanation. A reschedule between locations should re-evaluate the display time rather than preserve a bare “2:30 PM” string.
Recheck the facts under the write boundary
The proposal records the patient and slot versions it used. At confirmation, the service reads the current versions and requires them to match. In a PostgreSQL design, the transaction would follow a consistent lock order:
BEGIN;
SELECT * FROM proposals
WHERE tenant_id = :tenant AND id = :proposal
FOR UPDATE;
SELECT * FROM slots
WHERE tenant_id = :tenant AND id = :slot
FOR UPDATE;
-- Check actor, proposal revision, expiry, slot state and versions.
-- Insert appointment, outbox event and idempotency result.
COMMIT;
This is a transaction sketch, not a runnable migration for the original schema. The application must handle every failed check and preserve the uniqueness constraints. PostgreSQL row locks prevent concurrent changes to locked rows until transaction end, but they do not validate the business rules for us.
The reference lab uses SQLite's database-level write serialization instead. Two independent connections compete for the same slot in its test. Exactly one records an appointment; the other receives a conflict. That proves the local example's invariant, not the throughput of a production PostgreSQL deployment.
A hold is a product feature, not a free optimization
It is tempting to hold every slot while the user reads a preview. That can improve completion but reduce apparent availability when people abandon the page. I would introduce holds only after measuring how often review-time conflicts occur.
If a hold is needed, it should have a short explicit expiry, an owner, a quota and a visible countdown. Its expiry does not itself guarantee that a booking can proceed; the commit must still validate the hold and capacity. A worker should clean up abandoned holds, while the database query treats expired holds as inactive even if cleanup is delayed.
For a worked policy, consider a 90-second hold and at most two holds per coordinator. Those are proposed operating defaults. A clinic with long identity verification or interpreter-assisted calls may need a different policy. I would measure abandoned holds and denied availability alongside booking conversion.
Rescheduling spans two commitments
“Move this appointment” is not always a single API call. If the external system requires creating a new appointment and cancelling the old one, the order matters. Cancelling first can leave the patient without either slot; booking first may temporarily create two appointments.
| Stage | Durable evidence | Recovery |
|---|---|---|
| New slot prepared | Proposal with old and new appointment context | Revalidate or expire |
| New appointment accepted | External receipt | Continue cancellation or create an exception task |
| Old appointment cancelled | Cancellation receipt | Mark reschedule complete |
| Cancellation outcome unknown | Original request and correlation ID | Reconcile before retrying |
I would prefer a vendor-supported atomic reschedule operation where available. Otherwise the workflow needs a documented compensation policy, including who decides whether to keep or release the new appointment. The assistant must not invent that decision after a timeout.
Test the difficult calendar, then the load
My fixture set would include two staff members choosing one slot, an old browser tab confirming an expired proposal, a patient merge occurring after preview, a practitioner becoming unavailable and a timezone change between locations. I would test the same logical confirmation with the same key and with a different key.
Only after those state transitions behave correctly would I load-test availability and booking separately. Availability is mostly a read workload; booking is a short contention-sensitive transaction. A useful load report includes conflict rate, transaction duration, lock wait and successful bookings per second, not only HTTP requests per second.
The reference lab includes the two-connection race, proposal-version checks and timezone tests. They give the developer an executable starting point for the scheduling boundary.