System Design Library

Scheduling (Calendly)

Let people book time on someone's calendar without double-booking.

5 min read·9 sections
Open the interactive version → diagrams, practice & more

Requirements

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

Common questions

How do you design Scheduling (Calendly)?

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.

What are the key components of Scheduling (Calendly)?

Availability engine · booking (transactional) · calendar sync

What scale numbers matter for Scheduling (Calendly)?

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…

What breaks Scheduling (Calendly) at scale?

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…

Part of System Design Library on SystemLore — system design explained with 148 deep topics, interactive diagrams, and a build-it-yourself game. Browse the glossary and "X vs Y" comparisons, or build this one →