Bitmask Calendars: How I Built Fast, Timezone-Safe Slot Availability in Flutter

The scheduling engine behind Drivers Lesson, a driving-school platform I lead as Lead Mobile App Developer at RentYard.

Drivers Lesson lets a student pick a driving instructor, see which time slots are actually free, book one, and pay for it — with 3DS card verification in the loop. The booking screen looks simple: a calendar, a list of times, a confirm button. Almost none of the actual difficulty is in that UI. It's in deciding, correctly and quickly, which slots to show in the first place.

Three things made this harder than a typical "pick a time" flow:

  • Instructors set recurring weekly availability, not per-date availability — so "is 3pm on March 14th free" is really a question about a recurring pattern, not a row in a table.
  • A slot can be blocked by more than one thing at once: an existing booking, an instructor's day off, or an "extra session" a student added on top of their regular package.
  • Students, instructors, and our backend clock are not all in the same timezone.

Why a bitmask instead of a list of booleans

My first instinct — and probably yours — is a list of 48 booleans per day (one per 30-minute slot). It works, but it gets expensive fast once you're computing availability for a week of instructors client-side, filtering it, then re-computing it every time a booking is added or an extra session bottom sheet opens.

Representing a day's availability as a single integer bitmask turned that into cheap bitwise arithmetic instead. Each bit is one time slot; a set bit means "open."

// One int represents a full day of 30-minute slots.
// Bit 0 = 00:00–00:30, bit 1 = 00:30–01:00, ... bit 47 = 23:30–24:00
int dayMask = instructorBaseAvailability; // e.g. 0x00FFFF00...

// Remove slots already booked
dayMask &= ~bookedSlotsMask;

// Remove slots blocked by a day off
if (isDayOff) dayMask = 0;

// Check if a specific slot (index) is free
bool isSlotFree(int mask, int slotIndex) =>
    (mask & (1 << slotIndex)) != 0;

Combining an instructor's recurring template, existing bookings, and one-off blocks becomes a handful of & and ~ operations instead of nested loops over lists. That mattered once the "extra sessions" feature landed — a student can book bonus sessions on top of their normal package, which meant merging availability from two different sources at once and deduplicating any overlap before rendering the picker sheet.

Cross-session deduplication

The tricky part wasn't the math, it was that "extra session" slots and "regular package" slots come from two separate API responses, and the same physical slot can appear in both if a student already has a regular lesson booked in a window they're also browsing for an extra one. I settled on merging both into masks first, then treating any bit set in the "already booked" mask as unavailable everywhere — regardless of which source flagged it.

Where this lives in the app: the mask logic sits behind a Riverpod provider that exposes only the final, already-merged availability to the UI. The draggable course-picker bottom sheet never sees "regular" vs "extra" — it just asks "is this slot free," which keeps the widget layer boring on purpose.

The timezone bug that actually bit us

Every scheduling app eventually ships a bug where a slot shown as "2:00 PM" books as 8:00 PM somewhere. Ours came from comparing slot keys as plain date strings across a day boundary — a slot generated in the instructor's local time landed on a different calendar date than the same instant in the student's local time, so the deduplication logic above simply never matched it against an existing booking.

The fix wasn't clever, just disciplined: every slot is generated, stored, and compared as UTC internally, and converted to a local TimeOfDay only at the very last step before rendering. Filtering by "is this slot in the past" or "is this slot within the next 24 hours" happens on the UTC value, never on a formatted string.

// Compare in UTC, format for humans only at render time
final isPast = slot.startUtc.isBefore(DateTime.now().toUtc());
final label = DateFormat.jm().format(slot.startUtc.toLocal());

Payments sit downstream of availability, not beside it

Once a slot is confirmed available, booking triggers a Stripe payment with 3D Secure verification. I treat this as a strictly sequential dependency: the app never lets a student initiate payment for a slot until the availability provider has re-confirmed it's still open, since the whole point of the bitmask model is that availability can change out from under a stale UI between "opened the sheet" and "tapped confirm."

What I'd tell a team building this today

  • Model availability as a small, composable data structure (bitmask, bitset, whatever your language makes cheap) before you reach for per-slot booleans in a list.
  • Do every comparison in UTC. Convert to local time exactly once, as late as possible, purely for display.
  • Keep the "is this slot bookable right now" check as a re-fetch immediately before payment, not a cached value from when the sheet opened.