Building a timezone-aware availability engine
How I modeled bookable time as interval math — lead-time buffers, time-off blocks and DST — and kept it all unit-testable.
Every booking product eventually runs into the same deceptively hard question: given a service, a schedule, and everything already on the calendar — what slots can a customer actually book right now?
For Atelier Eri I wanted that logic to be one pure function I could trust. Here's how I got there.
Model time as intervals, not slots
The naive approach generates a grid of 30-minute slots and marks each one free or busy. It works until it doesn't — variable service durations, buffers, and overlapping blocks turn it into a mess of off-by-one bugs.
Instead I represented everything as half-open intervals [start, end) and reduced the problem to set arithmetic:
availability = workingHours
− bookings
− timeOff
− leadTimeBuffer(now)
Subtraction of interval sets is small, total, and easy to reason about. Each rule became its own function that takes intervals and returns intervals.
Lead time and DST are where it breaks
Two things quietly destroy naive implementations:
- Lead-time buffers — you can't book 5 minutes from now. The buffer is just another interval subtracted from the front, anchored to
now. - Daylight saving time — doing math on local wall-clock strings will eventually drop or duplicate an hour. I kept everything in UTC instants and only formatted to the salon's timezone at the very edges.
Make it testable by keeping it pure
Because the engine takes data in and returns data out — no database, no clock, no network — the test suite is just tables of inputs and expected outputs. now is a parameter, not Date.now(). That single decision made DST and lead-time edge cases trivial to pin down with Vitest.
If a function reaches for the wall clock or the database, it's no longer testable in isolation. Push those to the boundary.
The payoff: when a real booking conflict shows up in production, I can reproduce it as a one-line test instead of guessing.