Skip to main content

Design a Calendar System Like Google Calendar

Google Calendar coordinates billions of meetings, appointments, and reminders across hundreds of millions of users and global timezones. The core architectural challenge lies in modeling complex recurring events (e.g. "every second Tuesday of the month except holidays") without denormalizing infinite database rows, handling Daylight Saving Time (DST) shifts smoothly, managing multi-participant RSVP state machines, and executing concurrent double-booking conflict detection.


1. Understanding the Problem

Functional Requirements

  1. Event Creation & Management: Create, edit, and delete single and recurring events with title, description, location, and conference links.
  2. Complex Recurrence Rules (RRULE): Support recurring patterns (daily, weekly, monthly, custom rules conforming to RFC 5545).
  3. Event Exceptions: Modify or delete a single instance of a recurring series ("only this instance") or truncate future instances ("this and all following").
  4. Invitations & RSVP State Machine: Invite attendees; attendees receive notifications and respond with ACCEPTED, DECLINED, or TENTATIVE.
  5. Timezone & DST Awareness: Correctly display event times across participants in different time zones and handle Daylight Saving Time transitions.
  6. Reminders & Push Notifications: Schedule notifications 10 minutes prior to meeting start.

Non-Functional Requirements

  • Sub-50ms Calendar View Rendering: Fetching a monthly or weekly calendar view for a user must return in <50ms< 50\text{ms}.
  • Consistency: No phantom events; updates to meetings must be visible immediately to all attendees.
  • Conflict Detection: Detect overlapping meetings and room booking conflicts in real time.
  • Scale: Support 500+ Million active users storing 10+ Billion events.

Capacity Estimations & Sizing (5 Years)

  • Active Users: 500 Million users.
  • Events per User: Average 20 events/month β€…β€ŠβŸΉβ€…β€Š240Β events/user/year\implies 240\text{ events/user/year}.
  • Total Events Stored (5 Years): 500MΒ usersΓ—240Γ—5Β years=600Β BillionΒ EventΒ Records500\text{M users} \times 240 \times 5\text{ years} = \mathbf{600\text{ Billion Event Records}}
  • Storage Calculations:
    • Most recurring events are represented by a single RRULE definition row, not individual instances!
    • Assuming 2 Billion active recurring rule rows + 5 Billion single events:
    • Average record size: 500 bytes.
    • Storage required: 7Β BillionΓ—500Β bytesβ‰ˆ3.5Β Terabytes7\text{ Billion} \times 500\text{ bytes} \approx \mathbf{3.5\text{ Terabytes}} (readily manageable on modern sharded relational databases like Spanner or PostgreSQL).
  • Throughput Sizing:
    • Read QPS (viewing calendar grids): Average 50,000 QPS, peaking at 150,000 QPS on Monday mornings.
    • Write QPS (creating/updating events): Average 5,000 QPS, peaking at 20,000 QPS.

2. The Set Up

Defining Core Entities

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ EVENT_MASTER β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ event_id β”‚ UUID β”‚ PRIMARY KEY β”‚
β”‚ calendar_id β”‚ UUID β”‚ Owner Calendar ID β”‚
β”‚ title β”‚ VARCHAR(256) β”‚ Event Title β”‚
β”‚ start_time_utc β”‚ TIMESTAMP β”‚ Base Start Time β”‚
β”‚ end_time_utc β”‚ TIMESTAMP β”‚ Base End Time β”‚
β”‚ timezone_iana β”‚ VARCHAR(64) β”‚ "America/New_York" β”‚
β”‚ is_recurring β”‚ BOOLEAN β”‚ Recurring Flag β”‚
β”‚ rrule β”‚ VARCHAR(256) β”‚ RFC 5545 Rule String β”‚
β”‚ conference_url β”‚ VARCHAR(512) β”‚ Google Meet URL β”‚
β”‚ version β”‚ INT β”‚ Optimistic Lock Rev β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ EVENT_EXCEPTION β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ exception_id β”‚ UUID β”‚ PRIMARY KEY β”‚
β”‚ parent_event_id β”‚ UUID β”‚ FK to EVENT_MASTER β”‚
β”‚ original_time_utcβ”‚ TIMESTAMP β”‚ Target Instance Date β”‚
β”‚ is_cancelled β”‚ BOOLEAN β”‚ True if Deleted β”‚
β”‚ new_start_utc β”‚ TIMESTAMP β”‚ Rescheduled Start β”‚
β”‚ new_end_utc β”‚ TIMESTAMP β”‚ Rescheduled End β”‚
β”‚ custom_title β”‚ VARCHAR(256) β”‚ Overridden Title β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ATTENDEE_INVITE β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ invite_id β”‚ UUID β”‚ PRIMARY KEY β”‚
β”‚ event_id β”‚ UUID β”‚ FK to EVENT_MASTER β”‚
β”‚ user_id β”‚ UUID β”‚ Attendee User ID β”‚
β”‚ rsvp_status β”‚ ENUM β”‚ NEEDS_ACTION, ACCEPT,β”‚
β”‚ β”‚ β”‚ DECLINE, TENTATIVE β”‚
β”‚ is_organizer β”‚ BOOLEAN β”‚ Meeting Host Flag β”‚
β”‚ updated_at β”‚ TIMESTAMP β”‚ Response Timestamp β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The API Design

1. Create Recurring Event​

POST /api/v1/calendars/{calendar_id}/events
Content-Type: application/json
Authorization: Bearer <user_token>

{
"title": "Weekly Engineering Standup",
"start_time": "2026-10-01T09:00:00",
"end_time": "2026-10-01T09:30:00",
"timezone": "America/Los_Angeles",
"rrule": "FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20270101T000000Z",
}

Response (201 Created):

{
"event_id": "9b1a8-4210-...",
"rrule": "FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20270101T000000Z",
"status": "CONFIRMED"
}

2. Get Calendar Events for Date Range (View Range)​

GET /api/v1/calendars/me/events?start=2026-10-01T00:00:00Z&end=2026-10-31T23:59:59Z

Response (200 OK):

{
"events": [
{
"event_id": "9b1a8-...",
"instance_id": "9b1a8-20261002T090000Z",
"title": "Weekly Engineering Standup",
"start": "2026-10-02T16:00:00Z",
"end": "2026-10-02T16:30:00Z",
"is_recurring_instance": true,
"rsvp_status": "ACCEPTED"
}
]
}

3. High-Level Design

Google Calendar Recurring Events & Distributed Invitation ArchitectureInteractive Topology
Read QPS
100K/sec
Write QPS
1K/sec
Latency SLA
< 15ms
5-Yr Storage
~15 TB
Active Scenario: User submits long URL -> Token Generator (KGS) allocates Base62 ID -> Writes to DB & Warm Cache
Client / AppBrowser / MobileAPI GatewayEnvoy / NGINXURL ServiceStateless Golang/JavaRedis CacheCluster (LRU)Primary DBPostgres / DynamoDBKafka / FlinkClick Analytics
Interactive Component Inspector
Click any architecture node on the SVG canvas to view under-the-hood engine mechanics, failure gotchas, and runtime tags.

Walkthrough of Core Flows

1. Fetching Calendar Views (Dynamic RRULE Expansion)​

  1. User opens the Calendar app in Month View (e.g. October 2026).
  2. The client requests all events falling between 2026-10-01 and 2026-10-31.
  3. The Calendar Service:
    • Queries the Primary Database for single events whose [start_time, end_time] overlaps the requested window.
    • Queries for all recurring event series (EVENT_MASTER) whose lifetime intersects the requested window.
    • Fetches all EVENT_EXCEPTION records associated with those recurring series.
  4. On-the-Fly Expansion Engine:
    • Runs an in-memory RFC 5545 generator (e.g. libical / Google RRule parser) for each recurring rule, generating occurrence timestamps inside the 31-day window.
    • Applies exceptions (replaces rescheduled instances, deletes cancelled instances).
    • Converts UTC timestamps into the user’s requested display timezone.
  5. Returns the consolidated list of occurrences in <30ms< 30\text{ms}.

2. The Invitation & RSVP Flow​

  1. When an organizer creates a meeting with 5 attendees, the system creates 1 EVENT_MASTER row and 5 ATTENDEE_INVITE rows in a single database transaction.
  2. An event is published to Apache Kafka (MeetingInvitedEvent).
  3. Downstream workers:
    • Dispatch interactive email notifications with RSVP buttons.
    • Send push notifications to mobile devices.
    • Automatically display the event tentatively on the attendees' calendars (status: NEEDS_ACTION).
  4. When an attendee clicks "Accept", an atomic update modifies rsvp_status = ACCEPTED on their invite record, broadcasting the updated status to all active attendees via WebSockets.

4. Potential Deep Dives & Bottlenecks

Deep Dive 1: Modeling Recurring Events (Why Never Pre-Generate Infinite Instances)

Should we generate physical database rows for every occurrence of a daily meeting for the next 10 years?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ PRE-EXPANSION VS DYNAMIC ON-THE-FLY EXPANSION β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ Option A: Pre-Expand into Physical Rows β”‚
β”‚ β€’ A daily meeting for 5 years = 1,825 database rows. β”‚
β”‚ β€’ Modifying the meeting time requires updating 1,825 β”‚
β”‚ rows in a single massive database transaction. β”‚
β”‚ β€’ Infinite recurring meetings crash database storage! β”‚
β”‚ β”‚
β”‚ Option B: Store Rule + Dynamic Expansion (Selected) β”‚
β”‚ β€’ Exactly ONE row stored in EVENT_MASTER: β”‚
β”‚ "FREQ=DAILY;INTERVAL=1" β”‚
β”‚ β€’ Modifying the series takes ONE row update! β”‚
β”‚ β€’ Instances are computed on-the-fly in RAM only for β”‚
β”‚ the 30-day window the user is currently viewing. β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Deep Dive 2: Handling Event Exceptions & Series Splitting

What happens when a user clicks: "Change time for this and all following events"?

Original Event Series (ID: E1):
[Mon 9am] ──► [Tue 9am] ──► [Wed 9am] ──► [Thu 9am] ──► [Fri 9am]
β–²
β”‚ User changes Thursday to 10am ("This and following")

The Split Sequence:
1. Update E1 RRULE: Add `UNTIL = Wednesday 23:59:59Z`.
(E1 now strictly covers Mon, Tue, Wed).
2. Create New Event Series (ID: E2):
Start Time = Thursday 10:00:00Z.
RRULE: Copy original recurrence rules.
3. Both series link via `original_parent_id = E1` for audit history.
  • "Only this instance" Exception: Stored as a single row in EVENT_EXCEPTION referencing (parent_event_id, original_start_date). The generator overrides that single occurrence during on-the-fly rendering.

Deep Dive 3: Timezone Math & Daylight Saving Time (DST) Bugs

Why is storing events purely in UTC a catastrophic trap for calendar systems?

  • The DST Shift Trap:
    • A user in New York creates a daily meeting for "9:00 AM New York time".
    • In summer (EDT), 9:00 AM is 13:00 UTC.
    • In winter (EST), 9:00 AM is 14:00 UTC.
    • If the database stores only 13:00 UTC, when DST changes in November, the meeting will suddenly ring at 8:00 AM New York time, enraging the user!
  • The Solution: Store three distinct pieces of metadata:
    1. start_time_local: 09:00:00 (wall-clock time).
    2. timezone_iana: "America/New_York" (exact IANA Timezone Database string, not a static offset like -05:00).
    3. start_time_utc: Calculated based on the specific date's DST rules. When the recurrence engine evaluates an occurrence on November 15, it consults the IANA Olson database to calculate 14:00 UTC, preserving the local wall-clock meeting at 9:00 AM sharp.

Deep Dive 4: Concurrent Double-Booking Prevention (Room Reservation)

How do we guarantee that two users cannot book the same conference room simultaneously?

-- Atomic Check-and-Insert with Row Locking
BEGIN TRANSACTION;

-- Lock overlapping confirmed room bookings
SELECT id FROM room_bookings
WHERE room_id = :room_id
AND status = 'CONFIRMED'
AND (start_time < :requested_end AND end_time > :requested_start)
FOR UPDATE;

-- If rows returned > 0, abort transaction: CONFLICT!
-- Otherwise, insert:
INSERT INTO room_bookings (room_id, event_id, start_time, end_time, status)
VALUES (:room_id, :event_id, :requested_start, :requested_end, 'CONFIRMED');

COMMIT;
  • Distributed Redis Lock Fallback: Alternatively, an in-memory distributed lock can be acquired on lock:room:{room_id}:{date_bucket} using Redlock or Redis Lua script to serialize bookings before touching the database.

5. Architectural Trade-Off Matrix

Design AlternativeOption AOption BSelected Choice & Rationale
Recurrence ModelingPre-generate all physical rowsStore RFC 5545 RRULE + Dynamic ExpandDynamic RRULE Expansion: Slashes database storage by 99%; updating series takes 1 query instead of thousands.
Time RepresentationPure UTC TimestampsWall-clock Local Time + IANA Timezone IDLocal Time + IANA Timezone: Prevents meetings from shifting by 1 hour during Daylight Saving Time (DST) transitions.
Calendar Storage EngineDocument Store (MongoDB)Globally Distributed SQL (Spanner/Postgres)Relational SQL: Strict ACID transactions required for room booking concurrency and multi-attendee RSVP state consistency.
Notification SchedulingCrontab scanning entire DB every minHierarchical Timing Wheel / Delayed QueueTiming Wheel / Kafka Delay: Memory-efficient O(1)O(1) timer dispatch; eliminates massive database full-table polling sweeps.

6. What is Expected at Each Level?

Mid-Level (L4 / IC4)

  • Clarifies functional requirements for single vs recurring events.
  • Designs relational schemas for events, attendees, and RSVP statuses.
  • Explains why time zones must be considered when storing event start times.

Senior (L5 / IC5)

  • Explains why pre-expanding infinite recurring events into physical database rows is an anti-pattern.
  • Details dynamic RFC 5545 RRULE expansion and exception handling ("this instance" vs "this and all following").
  • Formulates correct timezone handling using IANA timezone identifiers to survive DST transitions.
  • Designs optimistic and pessimistic locking mechanisms to prevent double-booking meeting rooms.

Staff+ (L6 / Principal)

  • Evaluates multi-tenant calendar integration across enterprise federations (e.g. Google Calendar syncing with Microsoft Exchange/Outlook over CalDAV).
  • Designs distributed notification pipelines using hierarchical timing wheels to dispatch millions of reminders per minute without thundering herd effects.
  • Formulates multi-region active-active database replication strategies with cross-region conflict resolution.
  • Evaluates smart scheduling algorithms (finding free meeting slots across 50 busy participants in <100ms< 100\text{ms}).
πŸ“–
Track Page Progress0 / 635 Read
Knowledge Base Completion0%