Our fresh-grocery mini program had run for years when we added cold-chain delivery in spring 2023. The new thing wasn't a page — it was time slots.
Where the Three Slot Tiers Came From
The client runs three vans: a morning-market tier from 5 to 7 a.m., a late-morning tier, and an afternoon tier. Each van's capacity is fixed; when it's full, orders roll to the next tier. Before, we only decremented product stock. Now there's another layer: the vegetables may be there, but if the van is full, the order can't be accepted.
Version one treated slots as a product attribute: check remaining seats at checkout, decrement if enough. The classic bug — read-then-write — meant two people picking the last slot of the day could both succeed.
We switched to a conditional update:
UPDATE delivery_slot SET booked = booked + 1 WHERE id = :slot AND booked < capacity
If zero rows are affected, we reply "this slot is full — try the next one", and the frontend grays out full slots in the picker. Two columns, capacity and booked, plus a slot_date: three rows per day.
Pre-Hold and Release
Ordering doesn't mean paying instantly. Creating an order pre-holds one seat; 15 minutes without payment releases it, with a scheduled job sweeping every 10 minutes. Same idea as the 2018 inventory pre-hold — except that time we held products, this time we hold vans.
One detail: canceling an order must release both product stock and the slot, and the two have to happen in one transaction. Version one wrote them separately; once, stock was released but the slot wasn't — the tier showed full while a seat sat empty until evening. We added the transaction, then re-audited 2,000-plus historical orders and found 3 similar cases.
Where the Number 24 Came From
The afternoon tier's capacity went from 30 to 24, and not by guesswork. For the first three days we recorded actual load per van: cold-chain boxes take more room than expected, and with 30 orders, five or six always ended up wedged in the aisle — the driver said they tip over in transit. At 24, the remaining 6 positions are reserved for what he calls "awkward packages".
We later added a max_cold column tracking how many cold-chain boxes fit, counted separately from regular packages. What a slot can exceed isn't the total order count — it's the cold-box count. Version one never anticipated this constraint; we only understood it after the driver posted a photo of his trunk in the group chat.
About Rescheduling
The client originally wanted users to change their delivery slot after ordering. We didn't build it; we went with "cancel and reorder". The reason is practical: rescheduling means releasing the old slot and holding the new one, and any failure in between leaves you holding neither or both. Cancel-and-reorder adds two steps, but every step is simple and easy to debug.
When someone actually calls asking to change, support cancels for them in the backend, places a new order, and manually re-applies the discount. Seven or eight times a month — cheaper than building a rescheduling flow.
Those 3 Inconsistent Orders
The transaction fix surfaced 3 historical orders where product stock was released but the slot wasn't — all old data from before the nightly batch job. We manually decremented booked and added a note to each. After the 2,000-plus-order audit, the script output a 4-column table; we printed it, signed it, and filed it in the project folder.
The morning tier still loads at 5 a.m.; the 4:40 export job now has a failure alert — a paper jam sends a message, and drivers read the plain-text version on their phones instead of copying by hand. The max_cold column was added in March 2023; since then, no more aisle-cramming on the afternoon tier, and the driver never posted another trunk photo.
The Driver's Sheet
Before departure, drivers need a sorting sheet split into three blocks by slot, each block ordered by delivery sequence. We built an export: one slot per A4 page, 11pt type, addresses on a single line, signature space at the bottom.
The morning tier is the tightest: load at 5, print by 4:40. One day the printer jammed at 4:45 and the driver copied 8 rows off the screen by hand. We later added an "export as plain text" button in the backend that works on a phone too.
Launched February 10; the first week brought two changes: the afternoon tier's capacity from 30 to 24 because it genuinely didn't fit, and the "full" message — originally "unavailable", which the client found too blunt, changed to "This slot is full — how about another time?"

