Design Calendly: System Design & Architecture

Build a scheduling platform where users can share availability, allow others to book meetings seamlessly, and integrate with external calendar systems while handling time zones and conflicts.

Scope

In Scope

  • Host flow: set their available time blocks, then share a public booking link
  • Guest flow: view available slots and book without an account (identified by email)
  • Calendar integration with Google Calendar, Outlook, and other major providers
  • Time zone handling, availability rules, and automated conflict prevention
  • Booking lifecycle: confirmation, reminders, and cancellation

Out of Scope

  • Rescheduling an existing booking (a reschedule is effectively a cancel plus a new booking, so we leave it out to keep the flow focused on create and cancel)
  • Payment processing for paid consultations and premium scheduling features
  • Building the video conferencing platform itself; we only call provider APIs (Zoom/Teams/Meet) to generate meeting links
  • Building an analytics and reporting dashboard UI; collecting raw booking metrics is still in scope
  • AI-powered meeting transcription and automatic note-taking capabilities

Functional Requirements

  • User connects an external calendar → system completes OAuth and imports existing busy times
  • User sets availability → system creates booking rules with time slots and preferences
  • User shares booking link → system generates public scheduling page with available times
  • Visitor selects time slot → system validates availability and creates temporary hold
  • Visitor confirms booking → system creates meeting and syncs with integrated calendars
  • System sends notifications → automated reminders and confirmations via email/SMS
  • User modifies availability → system updates rules and re-checks already-booked meetings for new conflicts
  • Meeting gets cancelled → system removes from all calendars and notifies participants

Non-functional Requirements

  • Availability lookup latency < 200ms P95 for real-time booking page loading
  • 99.95% booking success rate for valid requests on available time slots
  • Calendar sync within 30 seconds of booking confirmation across all platforms
  • Time zone accuracy with automatic DST handling and global coverage
  • A booking still succeeds when an external calendar provider is down; sync is queued and retried
  • Booking data durability 99.999% with immediate cross-region backup
  • Handle 1000+ concurrent booking attempts for the same host/slot without race conditions

Back-of-the-Envelope Estimates

  • Assume 50M users, 10M bookings/day, 500M availability checks/day (50:1 read/write ratio)
  • Peak traffic: ~1M bookings/hour during business hours (~80% of daily volume in an ~8h window)
  • Calendar integrations: 100M API calls/day to Google/Microsoft with rate limiting
  • Average booking: 30-minute meetings, 2 participants (host + guest), 24-hour advance notice
  • Notification volume: 30M reminder emails/day, 5M SMS notifications/day

💡 How the read/write ratio shapes our database choice

With roughly 50 availability checks for every 1 booking, this is a read-heavy system. That points us to two complementary stores: a relational database (PostgreSQL) as the durable source of truth for users and bookings, where we want strong consistency and transactions on the (relatively rare) writes, plus a Redis cache in front of it to absorb the huge volume of availability reads. Because writes are infrequent, paying for transactional guarantees on the write path does not hurt overall throughput, while caching keeps read latency low.

High-level Architecture

  • API Gateway: Authenticates hosts and applies rate limiting and abuse protection on public booking pages
  • Availability Service: Computes a host's open time slots and serves them to booking pages, with in-process time zone/DST handling via a standard time zone library and caching
  • Rules Engine: Part of the Availability Service that turns a host's available time blocks into the concrete open slots a guest can pick from
  • Booking Service: Conflict-free reservation system using the hold-and-commit pattern
  • Hold Manager: Part of the Booking Service that places a short-lived reservation on a slot the instant a guest selects it, so two guests cannot grab the same slot at once
  • Conflict Checker: Part of the Booking Service that, before confirming, verifies the slot does not overlap an existing booking or a busy time synced from the host's calendar
  • Calendar Integration: Keeps a host's external calendars (Google/Outlook) in sync in both directions
  • Rate Limiter: Throttles outbound calls to external calendar APIs so we stay under each provider's limits (token bucket with backoff)
  • Webhooks: Endpoint that receives change notifications from external calendars, so updates appear quickly without us constantly polling
  • Sync Queue: Durable message queue that holds calendar-sync jobs (after a booking) and inbound webhook events, so they are processed asynchronously instead of blocking the booking response
  • Sync Workers: Background consumers that drain the queue - pushing events to Google/Outlook and applying incoming calendar changes - with retries, backoff, and a dead-letter queue (DLQ) for jobs that keep failing
  • Notification Service: Multi-channel reminder system with delivery tracking

Data/User Flow Diagram

        HOST                                 GUEST (no account)
   manage availability                     open booking link
          |                                        |
          +--------------> [ API Gateway ] <-------+
       (host auth; rate limiting + abuse protection on public pages)
                                 |
        +------------------------+------------------------+
        |                        |                        |
[Availability Service]    [Booking Service]       [Calendar Integration]
   |          |                  |                  |            |
[Rules]  [Slot Cache]     [Hold Manager]      [Webhooks]   [Rate Limiter]
[Engine]  (Redis)               |                  |            ^
   |                            v                  v            |
   |                    [Conflict Checker]    [ Sync Queue ]    |
   v                            |              (durable)        |
[Users / Rules DB] <----- [Booking DB] -------->  |            |
 (PostgreSQL)             (PostgreSQL)             v            |
                                |            [Sync Workers] ----+--> [External Calendars]
                                v            (retry/backoff/DLQ)
                       [Notification Service] --> [Email / SMS Gateway]

Outbound: confirm booking -> write Booking DB -> enqueue sync job -> worker pushes to Google/Outlook
Inbound:  provider webhook -> Sync Queue -> worker updates Calendar Events + invalidates availability cache
(Time zone/DST handled in-process via a standard time zone library inside Availability Service)

💡 Notification system is intentionally out of the deep dive

Designing a robust notification system (scheduling, multi-channel delivery, retries, and time zone-aware reminders) is a large problem in its own right and could be its own design question. We keep the Notification Service in the high-level architecture because bookings trigger confirmations and reminders, but everything below this point deliberately does not design its internals.

Data Model & Storage

  • Users (PostgreSQL): host accounts, availability rules, and calendar connections. We use SQL because this data is highly relational (bookings and calendar events all reference a user) and we want strong consistency and constraints. There is no extreme write volume here that would push us toward NoSQL.
  • Bookings (PostgreSQL): the source of truth for confirmed meetings. We use SQL because a booking must be transactionally consistent to avoid double-booking, and we rely on relational guarantees like foreign keys and a no-overlap constraint. NoSQL optimizes for very high write throughput, which we do not need and which would make these guarantees harder.
  • Availability Cache (Redis): pre-computed free/busy slots for a host, served to booking pages for fast (sub-200ms) reads. Has a TTL and is invalidated when a booking is made or availability rules change.
  • Booking Holds (Redis): a temporary reservation on a slot while a guest fills out the booking form. The 2-minute TTL auto-releases the slot if the guest abandons the form, so an incomplete booking never locks a slot forever.
  • Calendar Events (PostgreSQL): external busy times kept in sync via provider webhooks (Google/Outlook notify us when the host's calendar changes), so the conflict checker can block slots the host is already busy for.
  • Sync Jobs (PostgreSQL): one row per pending calendar sync, written in the SAME transaction as the booking. Writing them together guarantees a confirmed booking is never lost even if the external sync is delayed; workers consume these rows, retry with backoff, and move repeatedly-failing jobs to a dead-letter queue.

API Design

GET /v1/availability/:user_id
Retrieve available time slots for booking.
POST /v1/bookings/hold
Create temporary hold on time slot.
POST /v1/bookings/confirm
Convert hold to confirmed booking.
PUT /v1/users/:id/availability
Update availability rules and preferences.
GET /v1/bookings/:id
Retrieve booking details and status.
DELETE /v1/bookings/:id
Cancel booking and notify participants.
POST /v1/calendar/sync
Trigger manual calendar synchronization.
POST /v1/webhooks/calendar
Receive calendar change notifications.

Low-level Design

Booking flow (hold-and-commit), step by step:

  • 1) Guest selects a slot. Booking Service does an atomic "create this key only if it does not already exist" write in Redis (the SET NX command) on "hold:{host_id}:{start_time}" with a 2-minute TTL. Because only the first request can create the key, exactly one guest wins the slot; if the key already exists, someone else is taking it and we reject right away.
  • 2) The winning guest fills out the form (name, email, answers) while the hold keeps the slot reserved.
  • 3) On Confirm, a single Postgres transaction re-checks for conflicts and inserts the booking; the no-overlap exclusion constraint is the final guard if two requests somehow reach commit together.
  • 4) In the SAME transaction we also write a Sync Job row, so the calendar sync can never be lost.
  • 5) The hold is deleted (or just left to expire).
  • Failure handling: if the hold expires before the guest confirms, or the slot was taken in the meantime, the booking is rejected and the guest sees refreshed availability.

Availability computation (the read path), step by step:

  • 1) Start from the host's available time blocks for the requested date range.
  • 2) Subtract existing bookings and busy times synced from external calendars.
  • 3) Slice the remaining free time into slots of the requested duration.
  • The result is cached in Redis ("avail:{user_id}:{date}", 24h TTL); over 95% of booking-page loads are served from cache, which is how we keep page loads fast.
  • Cache invalidation: when a booking is made or availability changes, only the affected dates for that host are invalidated, not the whole cache.

Conflict detection:

  • At confirm time the Conflict Checker tests whether the requested interval overlaps any existing confirmed booking or any synced busy time for that host.
  • The overlap test is a simple interval comparison: new.start < existing.end AND new.end > existing.start.
  • The database exclusion constraint enforces the same rule durably, so even a race that slips past the Redis hold can never persist two overlapping bookings.

Async calendar sync (queue + workers):

  • Booking confirmation does NOT call Google/Outlook inline - it writes a Sync Job in the same booking transaction and returns immediately, so a slow or down provider never blocks or fails a booking.
  • Background Sync Workers pull jobs from the durable queue and call the external calendar through the Rate Limiter (token bucket + exponential backoff) to stay under provider limits.
  • A job that keeps failing after several retries is moved to a dead-letter queue (DLQ) for inspection instead of blocking the queue.
  • Inbound: when a provider sends a webhook (the host changed their own calendar), we verify the signature, enqueue it, and a worker re-fetches the changed event, updates the Calendar Events table, and invalidates the affected availability cache - usually within 30 seconds.
  • Webhooks are processed idempotently (dedupe on the provider event id) because providers can deliver the same notification more than once.

Database Types and Schemas

PostgreSQL tables

- Users
  user_id UUID PRIMARY KEY,
  username VARCHAR(50) UNIQUE NOT NULL, -- public handle, e.g. calendly.com/jane
  email VARCHAR(255) UNIQUE NOT NULL,
  time_zone VARCHAR(50) DEFAULT 'UTC',
  availability_rules JSONB, -- the host's available time blocks
  calendar_connections JSONB, -- encrypted Google/Outlook OAuth tokens
  booking_preferences JSONB, -- min notice, max future booking
  created_at TIMESTAMP DEFAULT NOW()
  -- username & email are UNIQUE, so Postgres already indexes them

- Bookings
  booking_id UUID PRIMARY KEY,
  host_id UUID REFERENCES users(user_id),
  guest_email VARCHAR(255) NOT NULL,
  guest_name VARCHAR(255),
  guest_timezone VARCHAR(50),
  guest_answers JSONB, -- responses to the host's intake questions
  start_time TIMESTAMP WITH TIME ZONE,
  end_time TIMESTAMP WITH TIME ZONE,
  status booking_status_enum DEFAULT 'confirmed', -- confirmed, cancelled
  calendar_event_ids JSONB, -- external calendar event references
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(), -- updated on cancel

  INDEX idx_host_bookings ON (host_id, start_time)
  -- DB-level safety net: two CONFIRMED bookings for the same host
  -- can never overlap in time, even if the Redis hold check is bypassed
  EXCLUDE USING gist (
    host_id WITH =,
    tstzrange(start_time, end_time) WITH &&
  ) WHERE (status = 'confirmed')

- Calendar Events  -- external busy times imported for conflict checks
  event_id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(user_id),
  external_event_id VARCHAR(255), -- Google/Outlook event ID
  provider calendar_provider_enum, -- google, outlook, apple
  start_time TIMESTAMP WITH TIME ZONE, -- busy block start
  end_time TIMESTAMP WITH TIME ZONE,   -- busy block end
  last_synced TIMESTAMP DEFAULT NOW(),

  UNIQUE(user_id, external_event_id, provider)
  INDEX idx_busy_times ON (user_id, start_time, end_time)

- Sync Jobs (pending calendar syncs)
  job_id UUID PRIMARY KEY,
  booking_id UUID REFERENCES bookings(booking_id),
  job_type VARCHAR(20), -- push_event, cancel_event
  status VARCHAR(20) DEFAULT 'pending', -- pending, done, failed
  attempts INT DEFAULT 0,
  next_attempt_at TIMESTAMP, -- backoff schedule for retries
  created_at TIMESTAMP DEFAULT NOW(),

  INDEX idx_pending_jobs ON (status, next_attempt_at) -- workers poll this

Redis keys

- Availability Cache
  Key: "avail:{user_id}:{date}"
  Value: ["09:00-09:30", "10:00-10:30", "14:00-14:30"]
  TTL: 24 hours

- Booking Holds
  Key: "hold:{host_id}:{start_time}" -- keyed by host so the host can't be double-booked for that slot
  Value: {"hold_id": "hold_8f3e2d1c", "guest_email": "client@example.com"}
  TTL: 2 minutes (auto-expires if the guest does not confirm)

API Request/Response Payloads

GET /v1/availability
Request
GET /v1/availability/user_123?start_date=2025-01-15&end_date=2025-01-22&duration=30
Response
{
  "user_id": "user_123",
  "time_zone": "America/New_York",
  "available_slots": [
    {
      "date": "2025-01-15",
      "slots": [
        {"start": "09:00", "end": "09:30"},
        {"start": "10:00", "end": "10:30"},
        {"start": "14:00", "end": "14:30"}
      ]
    },
    {
      "date": "2025-01-16", 
      "slots": [
        {"start": "09:30", "end": "10:00"},
        {"start": "15:00", "end": "15:30"}
      ]
    }
  ],
  "booking_rules": {
    "min_notice_hours": 24,
    "max_future_days": 60
  }
}
POST /v1/bookings/hold
Request
{
  "host_id": "user_123",
  "start_time": "2025-01-15T09:00:00-05:00",
  "end_time": "2025-01-15T09:30:00-05:00",
  "guest_email": "client@example.com",
  "guest_name": "Jane Smith"
}
Response
{
  "hold_id": "hold_8f3e2d1c",
  "status": "held",
  "expires_at": "2025-01-14T10:32:00Z",
  "booking_url": "https://calendly.com/confirm/hold_8f3e2d1c"
}
POST /v1/bookings/confirm
Request
{
  "hold_id": "hold_8f3e2d1c",
  "guest_details": {
    "name": "Jane Smith",
    "email": "client@example.com",
    "phone": "+1-555-0123",
    "notes": "Discussing Q1 project requirements"
  },
  "meeting_preferences": {
    "location": "Google Meet",
    "send_reminders": true,
    "reminder_times": [24, 1] // hours before
  }
}
Response
{
  "booking_id": "booking_7a2b8f4e",
  "status": "confirmed", 
  "meeting_details": {
    "start_time": "2025-01-15T09:00:00-05:00",
    "end_time": "2025-01-15T09:30:00-05:00",
    "location": "https://meet.google.com/abc-defg-hij",
    "calendar_event_id": "google_cal_event_123"
  },
  "confirmation_sent": true,
  "reminder_scheduled": true
}
PUT /v1/users/:id/availability
Request
{
  "working_hours": {
    "monday": {"start": "09:00", "end": "17:00"},
    "tuesday": {"start": "09:00", "end": "17:00"},
    "friday": {"start": "09:00", "end": "15:00"}
  },
  "meeting_preferences": {
    "default_duration": 30,
    "min_notice_hours": 24,
    "max_future_days": 60
  }
}
Response
{
  "user_id": "user_123",
  "availability_updated": true,
  "cache_invalidated": true,
  "affected_bookings": [],
  "next_available_slot": "2025-01-15T09:00:00-05:00"
}
DELETE /v1/bookings/:id
Request
DELETE /v1/bookings/booking_7a2b8f4e
Response
{
  "booking_id": "booking_7a2b8f4e",
  "status": "cancelled",
  "cancelled_at": "2025-01-14T10:45:00Z",
  "calendar_events_removed": [
    {"provider": "google", "event_id": "google_cal_event_123"},
    {"provider": "outlook", "event_id": "outlook_event_456"}
  ],
  "notifications_sent": {
    "host_notified": true,
    "guest_notified": true,
    "cancellation_email_sent": true
  },
  "slot_released": true
}

Concluding Discussion

Potential Bottlenecks

  • Issue: Booking volume spikes during peak business hours, and Google/Microsoft calendar API rate limits cause sync delays under that load.
    Mitigation: Token-bucket rate limiting (picture a bucket that refills with a fixed number of "permits" every second; each call to the external calendar uses one permit, and when the bucket is empty calls must wait - this allows short bursts while keeping the long-run rate under the provider's limit) with request batching and exponential backoff, plus burst/autoscaling capacity provisioned ahead of peak business-hour traffic.
  • Issue: Concurrent booking attempts on the same slot create race conditions.
    Mitigation: Atomic "create-only-if-absent" Redis holds (SET NX) with a short TTL and a hold-and-commit two-phase flow.

Single Points of Failure

  • Single availability cache instance without clustering causing booking page failures
  • Centralized calendar integration service without horizontal scaling limiting sync capacity
  • Single-region booking database without automated failover risking booking data loss

Security Improvements

  • Rate limiting and abuse prevention for booking attempts and availability scraping
  • Unguessable booking links so a host's page/slug can't be enumerated or scraped in bulk

Critical Knowledge

Time Zones and Daylight Saving Time:

  • Scheduling across time zones is one of the hardest parts of this problem: the guest and host are often in different zones.
  • Example: when a host offers "every Tuesday at 2 PM," daylight saving time means that is a different absolute moment in summer than in winter, and a guest in another country must see it in their own local time - so the system has to translate carefully to make the booking land at the time both people actually expect.

Meeting Link Generation and Management:

  • Automatic video conferencing link creation for confirmed bookings
  • Provider integration: Google Meet, Zoom, Microsoft Teams room creation via APIs
  • Link security: unique meeting IDs
  • Calendly automation: 90% of meetings get automatic video links without manual setup
  • Link lifecycle: create on booking confirmation, cleanup on cancellation

Multi-Calendar Aggregation:

  • A host can connect more than one calendar (for example a Google work calendar and an Outlook personal one), and we treat a slot as busy if any connected calendar is busy.
  • Example: a host has work, personal, and phone calendars; a meeting on any of them should block that time on their booking page.
  • We pull busy times from each connected calendar and merge them when computing availability, so the host is never double-booked across calendars.

Booking Page Performance:

  • Public booking pages are high-traffic and need to load fast everywhere, so the static parts (the page, scripts, images) are served from a CDN close to the user.
  • The availability shown on the page comes from the Redis cache rather than the database, keeping loads quick even under heavy traffic.
  • Together these let booking pages load quickly worldwide with a high cache hit rate.