System Design Case Study

How does Google Calendar handle recurring events across timezones?

?? Design a calendar recurrence engine: timezone-aware, exception handling, on-the-fly generation
Concepts Involved

Problem Statement

How does a calendar platform handle recurring events (every Tuesday, except holidays) across timezones, generating occurrences on-the-fly with exception handling without storing millions of individual instances?

Core challenge: "Every Tuesday at 10am" seems simple. But: which timezone? What about DST transitions? What if one occurrence is deleted? What if the series is modified "this and future"? Storing every instance wastes space. Computing on-the-fly requires complex RRULE evaluation.
1B+
calendar events
many are recurring
RRULE
recurrence spec
RFC 5545 (iCalendar)
500+
timezones (IANA)
DST rules change yearly
On-the-fly
occurrence generation
not pre-stored

Architecture · RRULE Engine + Exception Handling

LAYER 1 · STORAGE (Stored Event: 1 Row per Recurring Series) RRULE + DTSTART DTSTART: 2024-01-02T10:00 RRULE: FREQ=WEEKLY;BYDAY=TU UNTIL: 2025-12-31 (optional) 1 row = entire series TZID America/New_York Store LOCAL time + TZID NOT UTC (DST breaks UTC) IANA tzdata for conversion EXDATE List EXDATE: 2024-03-19T10:00 EXDATE: 2024-07-04T10:00 Deleted single occurrences Skip during expansion Overrides RECURRENCE-ID: Mar 26 ? moved to 11:00, new title Modified single instance Separate row, linked to parent query window (e.g., March 2024) LAYER 2 · ENGINE (RRULE Expansion Engine) Expand Window Generate dates within requested range only Never unbounded! RFC 5545 compliant Apply EXDATEs Remove deleted dates from generated list O(n) filter pass Match by DTSTART Merge Overrides Replace matching date with override data New time/title/location Linked by RECURRENCE-ID Handle DST Detect DST boundary Keep wall-clock time 10am EST ? 10am EDT UTC offset changes Convert TZID?UTC For cross-TZ display Use latest IANA tzdata Compute at query time Not pre-stored LAYER 3 · OUTPUT (Generated Occurrences + "This and Future" Split) Generated Occurrences (March 2024) ? Mar 5 (Tue) 10:00 EST · normal instance ? Mar 12 (Tue) 10:00 EDT · DST changed (UTC-5?UTC-4) ? Mar 19 (Tue) · SKIPPED / EXDATE ? Mar 26 (Tue) 11:00 EDT · overridden (moved to 11:00) "This and Future" Split Visualization Original Series RRULE: WEEKLY;BYDAY=TU + UNTIL=2024-04-15 Jan 2 ? Apr 9 (ends) 10:00 AM unchanged SPLIT New Series DTSTART: 2024-04-16 11:00 RRULE: WEEKLY;BYDAY=TU Apr 16 ? forever New time: 11:00 AM Store local time + TZID NOT UTC | DST: 10am EST ? 10am EDT (same local, different UTC) Never unbounded expansion | IANA tzdata updated yearly · countries change DST rules | Cache materialized windows (next 6 months)
ConceptStorageHow It Works
Recurring Event1 row: RRULE + DTSTART + TZIDRRULE:FREQ=WEEKLY;BYDAY=TU;UNTIL=20251231 · generate occurrences on query
Exception (delete one)EXDATE list on parentEXDATE:20240319T100000 · skip this occurrence during generation
Exception (modify one)Separate event with RECURRENCE-IDOverride instance: different time/title but linked to parent series
"This and future"Split into 2 seriesOriginal gets UNTIL before split. New series starts from split point with new RRULE.
TimezoneStore as local time + TZID"10:00 America/New_York" · compute UTC at query time using current TZ rules
DST transitionTZID handles automatically10am EST ? 10am EDT (same local time, different UTC offset). Wall-clock time preserved.
Why not store all instances? "Every weekday forever" = infinite instances. Even "every day for 5 years" = 1,825 rows per event. With 1B recurring events, that's trillions of rows. Instead: store 1 RRULE, generate occurrences for the requested date range on-the-fly. Cache materialized windows (e.g., next 6 months).
Query pattern: "Show me events for March 2025" ? ? Fetch all non-recurring events in range ? Fetch all recurring events whose DTSTART = end AND (no UNTIL or UNTIL = start) ? Expand RRULEs within range ? Apply EXDATEs ? Merge overridden instances ? Sort by time.
Pitfalls: Storing in UTC · "10am meeting" shifts when DST changes (store local + TZID instead). Unbounded RRULE expansion · always limit to requested window. TZ rule changes · countries change DST rules; must use latest IANA tzdata. Floating time · "all-day events" have no timezone (date only).
Real-world: Google Calendar · RFC 5545 RRULE engine, stores local time + TZID. Outlook · similar, with Exchange-specific extensions. Apple Calendar · CalDAV + RRULE. Temporal · cron-based scheduling for distributed systems (different problem, same timezone challenges).

Interview Cheat Sheet

The 6 things to say for calendar/scheduling design

1. Store RRULE, not instances · "every Tuesday" = 1 row, generate occurrences on-the-fly
2. Store local time + TZID · never store UTC for recurring events (DST breaks it)
3. EXDATE for exceptions · delete one occurrence without breaking the series
4. "This and future" = split series · original gets UNTIL, new series starts from split point
5. Query: expand RRULEs within requested window · never unbounded expansion
6. IANA tzdata updates · countries change DST rules; must use latest timezone database