1. Requirements & Scope (5 min)

Functional Requirements

  1. Create, read, update, and delete events with title, description, time range, location, and attendees
  2. Support recurring events with complex patterns (every weekday, first Monday of each month, every 2 weeks)
  3. Handle timezone conversions correctly — events display in the user’s local timezone regardless of where they were created
  4. Detect scheduling conflicts when creating or accepting events
  5. Send notifications and reminders (email, push) at configurable times before events

Non-Functional Requirements

  • Availability: 99.99% — calendar outages directly cause missed meetings and lost productivity across entire organizations
  • Latency: < 200ms to render a week view (fetch all events for 7 days). Event creation < 300ms.
  • Consistency: Strong consistency for the event owner’s view — after creating an event, the owner must immediately see it. Eventual consistency (< 5 seconds) acceptable for other attendees seeing the event appear.
  • Scale: 500M active users, average 20 events/week per user, 50K event writes/sec peak, 200K calendar view reads/sec peak
  • Sync: Events must sync reliably across all devices (web, mobile, desktop) within 5 seconds

2. Estimation (3 min)

Traffic

  • Event writes (create/update/delete): 50K/sec peak, 20K/sec average
  • Calendar view reads: 200K/sec peak (Monday mornings are the spike)
  • Notification triggers: 100K/sec (reminders firing across timezones)
  • Recurring event expansion: done at query time for future dates, pre-expanded for past 30 days

Storage

  • 500M users x 20 events/week x 52 weeks = 520 billion events/year
  • But most events are non-recurring single events. Average active events per user: ~200 (upcoming + recent)
  • 500M users x 200 events x 500 bytes = 50 TB for active event data
  • Recurring event storage: only the RRULE is stored (not expanded instances)
    • 500M users x 10 recurring events avg x 200 bytes = 1 TB for recurring patterns
  • Total: ~51 TB for core event data

Calendar Views

  • Week view: fetch ~20-30 events per user (single-occurrence + expanded recurring)
  • Month view: fetch ~80-120 events per user
  • Most queries are time-range queries on a single user’s calendar → excellent for sharding by user_id

3. API Design (3 min)

// Create an event
POST /calendars/{calendar_id}/events
  Body: {
    "title": "Sprint Planning",
    "description": "Q1 sprint planning meeting",
    "start": "2026-02-23T09:00:00",
    "end": "2026-02-23T10:00:00",
    "timezone": "America/New_York",
    "location": "Conference Room A / https://meet.google.com/xyz",
    "attendees": [
      {"email": "[email protected]", "optional": false},
      {"email": "[email protected]", "optional": true}
    ],
    "recurrence": "RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=12",
    "reminders": [
      {"method": "popup", "minutes": 10},
      {"method": "email", "minutes": 30}
    ],
    "visibility": "default",
    "color_id": 5
  }
  Response 201: {
    "event_id": "evt_abc123",
    "calendar_id": "cal_user456",
    "html_link": "https://calendar.google.com/event?eid=abc123",
    "created": "2026-02-22T14:30:00Z",
    "updated": "2026-02-22T14:30:00Z",
    ...
  }

// Get events in a time range (calendar view)
GET /calendars/{calendar_id}/events?timeMin=2026-02-23T00:00:00Z&timeMax=2026-03-02T00:00:00Z&timezone=America/New_York&singleEvents=true
  Response 200: {
    "events": [
      {
        "event_id": "evt_abc123",
        "title": "Sprint Planning",
        "start": {"dateTime": "2026-02-23T09:00:00-05:00", "timezone": "America/New_York"},
        "end": {"dateTime": "2026-02-23T10:00:00-05:00", "timezone": "America/New_York"},
        "recurring_event_id": "evt_abc123",    // parent recurring event
        "original_start": "2026-02-23T09:00:00-05:00",
        "attendees": [...],
        "status": "confirmed"
      },
      ...
    ]
  }

// Update a single instance of a recurring event
PUT /calendars/{calendar_id}/events/{event_id}?instance=2026-03-02T09:00:00-05:00
  Body: { "start": "2026-03-02T10:00:00", "end": "2026-03-02T11:00:00" }

// Check free/busy time for scheduling
POST /freeBusy
  Body: {
    "timeMin": "2026-02-24T08:00:00Z",
    "timeMax": "2026-02-24T18:00:00Z",
    "items": [
      {"id": "[email protected]"},
      {"id": "[email protected]"}
    ]
  }
  Response 200: {
    "calendars": {
      "[email protected]": {
        "busy": [
          {"start": "2026-02-24T09:00:00Z", "end": "2026-02-24T10:00:00Z"},
          {"start": "2026-02-24T14:00:00Z", "end": "2026-02-24T15:00:00Z"}
        ]
      },
      ...
    }
  }

Key Decisions

  • singleEvents=true tells the API to expand recurring events into individual instances — critical for calendar rendering
  • Recurring event modifications are tracked as “exception instances” linked to the parent event
  • Free/busy API is a separate endpoint optimized for multi-user scheduling (returns only busy slots, not event details — respects privacy)

4. Data Model (3 min)

Events Table (MySQL/PostgreSQL, sharded by owner_user_id)

Table: events
  event_id            (PK) | bigint (Snowflake ID)
  calendar_id              | bigint
  owner_user_id            | bigint (shard key)
  title                    | varchar(500)
  description              | text
  start_time               | timestamp with timezone
  end_time                 | timestamp with timezone
  start_timezone           | varchar(50)    -- e.g., "America/New_York"
  end_timezone             | varchar(50)
  location                 | varchar(500)
  is_all_day               | boolean
  recurrence_rule          | varchar(500)   -- RRULE string, null if non-recurring
  recurrence_end           | timestamp      -- when the recurrence stops
  visibility               | enum(default, public, private)
  status                   | enum(confirmed, tentative, cancelled)
  color_id                 | tinyint
  created_at               | timestamp
  updated_at               | timestamp

Indexes:
  (calendar_id, start_time)  -- primary query: events in a time range for a calendar
  (owner_user_id)            -- shard key

Recurring Event Exceptions Table

Table: event_exceptions
  exception_id        (PK) | bigint
  parent_event_id          | bigint (FK → events)
  original_start_time      | timestamp   -- which instance is being modified
  is_cancelled             | boolean     -- true if this instance is deleted
  modified_title           | varchar(500)
  modified_start_time      | timestamp
  modified_end_time        | timestamp
  modified_location        | varchar(500)
  -- other overridden fields (null = use parent's value)

Attendees Table

Table: event_attendees
  event_id                 | bigint (FK → events)
  user_id                  | bigint
  email                    | varchar(200)
  response_status          | enum(needsAction, accepted, declined, tentative)
  is_optional              | boolean
  is_organizer             | boolean

Index: (user_id, event_start_time)  -- for attendee's calendar view

Reminders Table

Table: reminders
  reminder_id         (PK) | bigint
  event_id                 | bigint
  user_id                  | bigint
  method                   | enum(popup, email, sms)
  minutes_before           | int
  trigger_time             | timestamp   -- precomputed: event.start - minutes_before
  is_sent                  | boolean

Index: (trigger_time, is_sent)  -- for the reminder scheduler to efficiently find due reminders

Why These Choices

  • Sharded MySQL by owner_user_id — most queries are “my events this week” which hits a single shard. Cross-user queries (free/busy) require scatter-gather but are less frequent.
  • RRULE stored as string, expanded at query time — storing every instance of “every weekday forever” would be infinite. RRULE is compact and standards-based (RFC 5545).
  • Exception table for recurring modifications — cleanly separates the recurring pattern from per-instance changes. No need to duplicate the entire event for each modified instance.

5. High-Level Design (12 min)

Architecture

Client (Web / Mobile / Desktop)
  │
  │  REST API / WebSocket (for real-time sync)
  ▼
┌───────────────┐
│  API Gateway   │  (auth, rate limiting, routing)
└───────┬───────┘
        │
        ├──────────────────┬───────────────────┬──────────────────┐
        ▼                  ▼                   ▼                  ▼
┌───────────────┐ ┌─────────────────┐ ┌───────────────┐ ┌───────────────┐
│ Event Service  │ │ Recurring Event │ │ Notification   │ │ Free/Busy     │
│ (CRUD for      │ │ Expander        │ │ Service        │ │ Service       │
│  events)       │ │ (expands RRULE  │ │ (reminders,    │ │ (scheduling   │
│                │ │  into instances) │ │  invites)      │ │  queries)     │
└───────┬───────┘ └────────┬────────┘ └───────┬───────┘ └───────────────┘
        │                  │                   │
        ▼                  ▼                   ▼
┌──────────────────────────────────────────────────────┐
│              Shared Data Layer                          │
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │ Events DB     │  │ Attendees DB │  │ Reminders DB │ │
│  │ (MySQL,       │  │ (MySQL,      │  │ (MySQL,      │ │
│  │  sharded by   │  │  sharded by  │  │  sharded by  │ │
│  │  owner_id)    │  │  user_id)    │  │  trigger_time│ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
│                                                         │
│  ┌──────────────┐  ┌──────────────┐                    │
│  │ Cache (Redis) │  │ Sync Queue   │                    │
│  │ (user's week  │  │ (Kafka, for  │                    │
│  │  view cache)  │  │  cross-device│                    │
│  │              │  │  sync)       │                     │
│  └──────────────┘  └──────────────┘                    │
└─────────────────────────────────────────────────────────┘

Event Creation Flow

User creates "Sprint Planning, every Monday 9-10 AM ET, 12 weeks"
  │
  ▼
Event Service:
  1. Validate input (times, timezone, RRULE syntax)
  2. Store event with recurrence_rule = "RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=12"
     (single row in events table, NOT 12 rows)
  3. Store attendees in event_attendees table
  4. Compute reminders for next 30 days of instances:
     → Expand RRULE for next 30 days → 4-5 instances
     → For each: insert into reminders table with precomputed trigger_time
  5. Publish event to Kafka "event-changes" topic
  │
  ├─→ Notification Service:
  │     → Send email invitations to all attendees
  │     → Each attendee's calendar view cache is invalidated
  │
  └─→ Sync Service:
        → Push real-time update to all owner's connected devices (WebSocket)
        → Push update to attendees' devices

Calendar View (Week) Flow

User opens calendar for week of Feb 23 - Mar 1
  │
  ▼
API Gateway → Event Service:
  1. Check cache: Redis key "cal_view:{user_id}:2026-W09"
     → Cache hit (80% of the time): return cached events, done
  2. Cache miss:
     a. Query events table:
        SELECT * FROM events
        WHERE calendar_id = ?
          AND ((start_time BETWEEN ? AND ?)           -- non-recurring events in range
               OR recurrence_rule IS NOT NULL)         -- all recurring events (need expansion)
     b. For recurring events: expand RRULE to find instances in this week
     c. Join with event_exceptions: apply per-instance modifications, remove cancelled instances
     d. Query attendees table: get events where this user is an attendee
     e. Merge owner's events + attendee events
     f. Sort by start_time
     g. Cache result in Redis (TTL: 5 minutes)
  3. Return to client

Components

  1. Event Service: Core CRUD. Handles event creation, updates, deletion. Sharded by owner_user_id for write locality.
  2. Recurring Event Expander: Library/service that takes an RRULE + time range and produces concrete event instances. Uses RFC 5545-compliant parser. Handles complex rules like “last Thursday of every month” or “every 3rd day, excluding weekends.”
  3. Notification Service: Processes reminder triggers and attendee invitations. Polls the reminders table every 10 seconds for due reminders. Sends via email, push notification, or in-app popup.
  4. Free/Busy Service: Optimized for multi-user scheduling queries. Maintains a denormalized “busy slots” table per user (pre-computed from events). Returns only time ranges, no event details.
  5. Sync Service: Real-time sync across devices. Uses WebSocket connections for push updates. When an event changes, publishes to Kafka, which fans out to all connected devices of affected users.
  6. Cache Layer (Redis): Caches calendar views per user per week/month. Invalidated on event create/update/delete. Hit rate: ~80% (users repeatedly view the same week).

6. Deep Dives (15 min)

Deep Dive 1: Recurring Events and RRULE

The problem: Recurring events are the hardest part of a calendar system. “Every weekday” generates ~260 instances/year. “Every day forever” is infinite. We cannot store every instance. But users can modify individual instances (move Tuesday’s meeting to Wednesday, cancel one occurrence).

RRULE (RFC 5545):

Basic patterns:
  FREQ=DAILY                              → every day
  FREQ=WEEKLY;BYDAY=MO,WE,FR             → Mon, Wed, Fri every week
  FREQ=MONTHLY;BYDAY=1MO                 → first Monday of every month
  FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU       → last Sunday of March (DST transition!)
  FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,TH     → every other week on Tue and Thu

Termination:
  COUNT=12                                → 12 occurrences total
  UNTIL=20261231T235959Z                  → until end of 2026
  (neither)                               → forever (unbounded)

Complex:
  FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
    → last weekday of every month

Expansion algorithm:

function expand(rrule, dtstart, range_start, range_end):
  instances = []
  candidate = dtstart
  while candidate <= range_end:
    if candidate >= range_start:
      instances.append(candidate)
    candidate = next_occurrence(rrule, candidate)
  return instances

Key: we NEVER expand more than the requested time range.
  - Week view: expand for 7 days → typically 1-5 instances per recurring event
  - Month view: expand for 30 days → 4-22 instances
  - Year view: expand for 365 days → up to 365 instances (but lazy-loaded by visible range)

Exceptions (modified instances):

User moves one instance of "Weekly Sprint Planning" from Monday to Wednesday:

events table: (unchanged)
  event_id: 100, title: "Sprint Planning", rrule: "FREQ=WEEKLY;BYDAY=MO"

event_exceptions table: (new row)
  parent_event_id: 100
  original_start_time: 2026-03-02T09:00:00-05:00   -- the Monday being moved
  modified_start_time: 2026-03-04T09:00:00-05:00   -- Wednesday instead
  modified_end_time: 2026-03-04T10:00:00-05:00

User cancels one instance:
  parent_event_id: 100
  original_start_time: 2026-03-09T09:00:00-05:00
  is_cancelled: true

Rendering algorithm:
  1. Expand RRULE for the view's time range → [Mar 2, Mar 9, Mar 16, Mar 23]
  2. For each instance, check exceptions:
     - Mar 2: exception found → use modified time (Wed Mar 4)
     - Mar 9: exception found, cancelled → skip
     - Mar 16: no exception → use RRULE-generated time
     - Mar 23: no exception → use RRULE-generated time
  3. Result: [Mar 4 (moved), Mar 16, Mar 23]

“This and following” modifications:

User changes "Sprint Planning" from 9 AM to 10 AM starting March 16:
  1. Original event: keep, add UNTIL=2026-03-15 (end before March 16)
  2. New event: copy all fields, set start=2026-03-16T10:00:00, rrule=FREQ=WEEKLY;BYDAY=MO
  3. Now we have two events that together represent the full series

This is called "splitting" a recurring event. It is the standard approach
(Google Calendar, Outlook, and Apple Calendar all do this).

Deep Dive 2: Timezone Handling

The problem: Alice in New York creates a meeting at “9 AM ET.” Bob in London sees it at “2 PM GMT.” Daylight Saving Time changes these offsets twice a year. And DST transition dates differ by country.

Storage format:

ALWAYS store events in two forms:
  1. start_time: timestamp with timezone (absolute moment in time)
     "2026-03-08T14:00:00Z" (UTC)
  2. start_timezone: "America/New_York"

Why both?
  - The UTC timestamp tells us the absolute moment
  - The timezone tells us the user's intent: "9 AM in New York"

When DST changes (Mar 8 in US, Mar 29 in EU):
  - Event at "9 AM ET": before DST → UTC-5. After DST → UTC-4.
  - If we only stored UTC, recurring event expansion would be wrong:
    Pre-DST Monday: 9 AM ET = 14:00 UTC
    Post-DST Monday: 9 AM ET = 13:00 UTC ← different UTC time!
  - By storing timezone + local time, we re-compute UTC for each instance.

Recurring event expansion with timezone:

Event: "9 AM America/New_York, every Monday"

Expansion for Mar 2 - Mar 16:
  Mar 2 (pre-DST):  9:00 AM ET = 2026-03-02T14:00:00Z (UTC-5)
  Mar 9 (DST transition on Mar 8): 9:00 AM ET = 2026-03-09T13:00:00Z (UTC-4)
  Mar 16: 9:00 AM ET = 2026-03-16T13:00:00Z (UTC-4)

The user always sees "9 AM" — the UTC equivalent shifts by 1 hour.
This is correct behavior: the meeting is at 9 AM local time regardless of DST.

All-day events:

Special case: "All day, Feb 23"
  - NOT stored as 00:00-23:59 in a specific timezone
  - Stored as a date (not datetime): "2026-02-23"
  - Displayed as "all day" in every timezone
  - A "birthday on Feb 23" should be Feb 23 everywhere, not Feb 22 in some timezones

Implementation: all-day events use DATE type, not TIMESTAMP.
  Display: starts at midnight local time, ends at midnight next day, in viewer's timezone.

Timezone database updates (IANA/Olson):

Problem: Governments change DST rules. Morocco changed DST dates in 2018. Russia abolished DST in 2014.
  If our timezone database is outdated, events will display at wrong times.

Solution:
  - Use the IANA timezone database (tzdata), updated ~10x/year
  - Deploy tzdata updates within 48 hours of release
  - Re-compute trigger times for all future reminders after a tzdata update
  - Calendar clients also need updated tzdata (iOS/Android ship theirs)

Deep Dive 3: Conflict Detection and Scheduling Assistance

Conflict detection on event creation:

When user creates/moves an event to Monday 9-10 AM:
  1. Query all events for this user on Monday 9-10 AM:
     SELECT * FROM events
     WHERE owner_user_id = ?
       AND start_time < '2026-02-23T10:00:00Z'
       AND end_time > '2026-02-23T09:00:00Z'
       AND status != 'cancelled'
  2. Also expand recurring events that fall in this range
  3. If overlapping events found:
     - Warn user: "Conflicts with 'Sprint Planning' at 9:00 AM"
     - Allow creation anyway (it's a warning, not a block)
     - Show conflict indicator (red/orange) on calendar UI

Efficient implementation:
  - Use the cached week view (already computed for calendar rendering)
  - Conflict check is a simple overlap query on the cached data
  - No additional DB query needed in most cases

Smart scheduling (find available time):

"Find a 1-hour slot for Alice, Bob, and Carol this week"

Algorithm:
  1. Fetch free/busy for all 3 users, Mon-Fri 9 AM - 5 PM
  2. Merge busy intervals per user:
     Alice: [9-10, 11-12, 14-15]
     Bob:   [9:30-10:30, 13-14]
     Carol: [10-11, 14-16]
  3. Compute union of all busy intervals:
     [9-12, 13-16] (merged)
  4. Find gaps ≥ 1 hour within working hours:
     [12-13] → suggest "Monday 12:00 - 1:00 PM"
  5. If no slots today, try Tuesday, etc.

Optimization:
  - Pre-compute daily free/busy bitmaps (1 bit per 15-min slot = 32 bits for 8-hour day)
  - AND the bitmaps: available = NOT(alice_busy OR bob_busy OR carol_busy)
  - Find runs of 4 consecutive 1-bits → available 1-hour slots
  - O(1) per day with bitmaps

7. Extensions (2 min)

  • Room/resource booking: Treat conference rooms as “users” with their own calendars. When creating an event with a room, check room availability and reserve it atomically. Support room capacity, AV equipment filtering, and auto-release if the organizer doesn’t check in within 10 minutes.
  • Calendar sharing and delegation: Support “view only,” “edit,” and “manage” permissions on calendars. Executive assistants can manage their boss’s calendar. Publish/subscribe calendars (public URLs for team calendars, holiday calendars, sports schedules).
  • CalDAV/iCal interoperability: Support the CalDAV protocol for third-party calendar client sync (Thunderbird, Apple Calendar). Export events in iCalendar (.ics) format. Import external calendars via URL subscription (auto-refresh every hour).
  • Working hours and out-of-office: Let users set working hours per day (e.g., Mon-Fri 9 AM - 5 PM). Smart scheduling respects working hours. Out-of-office events auto-decline new invitations with a custom message.
  • Analytics and meeting load: Dashboard showing weekly meeting hours, back-to-back meeting chains, and meeting-free focus time. Nudge users when meeting load exceeds a threshold (“You have 35 hours of meetings this week — consider declining some”).