1. Requirements & Scope (5 min)
Functional Requirements
- Create, read, update, and delete events with title, description, time range, location, and attendees
- Support recurring events with complex patterns (every weekday, first Monday of each month, every 2 weeks)
- Handle timezone conversions correctly — events display in the user’s local timezone regardless of where they were created
- Detect scheduling conflicts when creating or accepting events
- 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=truetells 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
- Event Service: Core CRUD. Handles event creation, updates, deletion. Sharded by owner_user_id for write locality.
- 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.”
- 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.
- 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.
- 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.
- 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”).