Scheduling (Calendly)
Let people book time on someone's calendar without double-booking.
Open the interactive version → diagrams, practice & moreRequirements
Functional
- Define availability
- Show open slots
- Book (no overlap)
- Calendar sync + timezones
Non-functional
- No double-booking
- Correct across time zones
Scale
Millions of bookings
The approach
Compute free slots from availability rules minus existing events; booking is a transactional slot reservation (atomic, no overlap); two-way sync with external calendars; times in UTC + original zone.
Key components
Availability engine · booking (transactional) · calendar sync
Numbers that matter
- A typical Calendly user connects 1–5 external calendars (Google, Outlook, iCloud) each polled or webhooks-pushed on change.
- Slot availability is computed over a rolling 60-day window — expanding to 90+ days multiplies the event fetch cost roughly linearly.
- Google Calendar API rate limit is ~1M queries/day per project — at scale, Calendly must batch and cache aggressively to stay under quota.
- A booking transaction (check slot free + insert reservation) must complete in <200ms to feel instant and prevent race-condition windows.
Senior deep-dive
Free/busy computation is the hard part — the booking form is trivial; computing which slots are actually open requires merging availability rules with all existing calendar events in real time.
Two-way calendar sync is the failure surface: OAuth tokens expire, external calendars drift out of sync, and a double-booking happens exactly when the sync was stale.
Booking is a transactional reservation — two people clicking the same slot simultaneously must not both succeed; a compare-and-swap or DB-level unique constraint on (owner, slot_start) is mandatory.
Availability computation: rules minus busy times
Each user defines an availability schedule ("Mon–Fri 9am–5pm, except lunch 12–1pm") stored as RRULE-like rules plus overrides. At booking time, the system generates all candidate slots in the window, then subtracts busy intervals fetched from connected calendars. The subtraction is an interval-merge operation — sort all busy intervals, merge overlapping ones, then diff against the candidate slots. This is O(n) in the number of events in the window and must be done per-visitor.
External calendar sync: webhooks vs. polling
Google Calendar supports push notifications (a webhook fires on any calendar change), which is the right mechanism — Calendly registers a watch channel per user calendar and receives near-real-time change events. But watch channels expire every 7 days and must be renewed; OAuth tokens expire and must be refreshed. A background sync-health job monitors channels, renews expiring watches, and falls back to polling every 5–15 minutes for users whose push channel has lapsed.
Booking transaction: the critical section
When a visitor confirms a slot, the system must atomically: (1) re-verify the slot is still free (re-fetch busy times from the external calendar), (2) insert a reservation row with a unique constraint on (owner_id, start_time), (3) add the event to the owner's calendar via API. Steps 1 and 3 involve external I/O, which means you can't hold a DB lock across them. The pattern: optimistically write the reservation (unique constraint handles races), then attempt the calendar insert — if the calendar insert fails, compensate by deleting the reservation and returning an error to the visitor.
Time zone handling: UTC + original zone
All times stored as UTC + the IANA time zone ID the user specified (e.g., "America/New_York"), never as a UTC offset (+05:30) alone — offsets change with DST but IANA zone IDs don't. Slot generation expands availability rules in the owner's time zone, then converts to UTC for storage. Display to the visitor re-converts from UTC to the visitor's local time zone (detected from browser). The trap: scheduling a recurring event across a DST boundary using a fixed UTC time will show the wrong local time half the year.
Multi-host and round-robin scheduling
Team scheduling ("book with any available sales rep") requires computing intersection of multiple users' free/busy and then distributing bookings fairly. Round-robin distribution tracks a last-booked timestamp per host and assigns to whoever was least recently booked among those currently available. This adds a distributed counter (Redis or DB atomic increment) to the booking critical path. For collective events (all hosts must be free), the intersection shrinks rapidly as you add more calendars — always show this upfront in the UX or users get frustrated by empty availability.
What breaks at scale
External calendar API rate limits are the first wall — Google Calendar's per-project quota means that at 100K users each loading availability pages, you exhaust quota instantly without aggressive per-user response caching (cache the freebusy response for 2–5 minutes). Webhook delivery failures (Calendly's server is down when Google pushes a change) leave stale availability until the next poll cycle — the double-booking window grows. The hardest failure: OAuth token refresh storms — if millions of tokens expire simultaneously (e.g., after a provider-side mass-revocation event), the queue of background refresh jobs spikes, sync lags, and bookings start double-booking silently.
In production
Calendly stores availability rules and existing Calendly bookings internally but relies on the user's external calendar (Google/Outlook via OAuth) as the authoritative busy-time source. Free/busy is fetched via the Google Calendar freebusy API (or Outlook equivalent) at booking-page load time and cached briefly. The real challenge is consistency across sync boundaries: if a user manually adds a meeting in Google Calendar, Calendly learns about it via a webhook push (Google Calendar push notifications) or the next poll — there's a window of seconds to minutes where Calendly thinks a slot is free but it isn't. Solving this without querying Google Calendar on every page load requires optimistic availability + a booking-time re-check.
Common mistakes
- Eventual consistency on slots (double-booking)
- Storing only UTC (DST bugs)
- Recomputing all slots on every view