Courses › System Design › Question Breakdowns

Design Google Calendar

Recurrence rules and exceptions, time zones, invitation fan-out, free/busy and reliable reminders.

Design a calendar service like Google Calendar: users create events (including recurring ones), invite others, see their week at a glance, check availability and get reminders. It looks like CRUD, but the interview is about modeling recurring events, time zones, fan-out of invitations and updates, free/busy queries and reliable reminders.

Requirements

Functional

  • Users can create, edit and delete events, including recurring events ("every weekday at 9:30", "monthly on the last Friday")
  • Users can invite others, who can accept / decline; organizers see responses
  • Users can view a day, week or month quickly, across multiple calendars
  • Users can check free/busy for a group to find a meeting time
  • Users get reminders (push / email) before events

Out of scope: room booking, video-call links, task lists, natural-language event creation.

Non-functional

  • Scale: ~1B users, ~100M daily active, ~10 events viewed per load
  • Fast reads: a week view loads in < 200ms; reads dominate writes (~100:1)
  • Correct times: an event at "9:00 New York" stays at 9:00 local time across daylight-saving changes
  • Reminders on time: within seconds of the requested moment, never duplicated, never skipped
  • Consistency: a user sees their own changes immediately; invitees within seconds

[!TIP] Store the rule, not every occurrence A daily recurring event with no end date has infinitely many occurrences. Store the recurrence rule (RFC 5545 RRULE) and expand it at read time for the window being viewed. Exceptions (one moved or cancelled occurrence) are stored separately. This single decision drives the data model.

Capacity estimates

Core Entities

  • Calendar — owner, name, time zone, sharing ACL
  • Event — calendar, organizer, title, start/end (with time zone ID), recurrence rule, version
  • Exception — event, original occurrence start, override (moved, edited) or cancelled
  • Attendee — event, user, response status
  • Reminder — event, user, offset (e.g. 10 minutes before), channel

API

POST /v1/calendars/{calId}/events
{ "title": "Standup", "start": "2026-10-12T09:30", "end": "2026-10-12T09:45",
  "timeZone": "America/New_York", "rrule": "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
  "attendees": ["ana@x.com", "raj@x.com"] }

GET  /v1/calendars/{calId}/events?from=2026-10-12&to=2026-10-19   → expanded occurrences
PATCH /v1/events/{eventId}?occurrence=2026-10-14T09:30&scope=this|following|all
POST /v1/events/{eventId}/rsvp   { "response": "accepted" }
POST /v1/freebusy   { "users": [...], "from": ..., "to": ... } → busy intervals per user

High-Level Design

user = client "Calendar Clients" @0,1
api = service "Calendar API" @1,1
db = store "Events DB\n(sharded by calendar)" @2,1
cache = cache "Expanded Weeks\nCache" @2,0
fan = queue "Change Events" @2,2
inv = service "Invitation /\nSync Service" @3,2
fb = service "Free/Busy Service" @3,0
rem = service "Reminder Scheduler" @3,1
notify = service "Push / Email" @4,1
user -> api
api -> cache
api -> db
db --> fan : changes
fan -> inv : update attendees' calendars
fan -> rem : (re)schedule reminders
fan -> fb : update busy index
rem -> notify
  • Writes go to the events DB, sharded by calendar ID (one user's events live together).
  • Every change emits an event that fans out to attendees' calendars, the reminder scheduler and the free/busy index.
  • Reads fetch the calendar's events overlapping the window, expand recurrences, apply exceptions, and cache the result.

Deep Dives

1. Recurring events and exceptions

The event row holds the series: first occurrence, duration, RRULE, time zone. A read for a window:

  • Query events whose series range overlaps the window (index on (calendar_id, series_start, series_end); open-ended series have series_end = ∞).
  • Expand each RRULE into occurrences within the window.
  • Apply exceptions: replace overridden occurrences, drop cancelled ones.

Editing "this and following": split the series — end the original series before that occurrence and create a new series starting there. Editing "only this one": add an exception row keyed by the occurrence's original start time.

2. Time zones

Store event times as local time + IANA time zone (09:30 America/New_York), not just UTC. Daylight-saving rules change, and a weekly 9:30 meeting must stay at 9:30 local after the clocks move — which is a different UTC time. Expand occurrences in the event's zone, then convert to the viewer's zone for display. All-day events have no time zone (they're dates).

[!WARNING] Precomputing UTC occurrences goes stale If you materialize occurrences as UTC timestamps, a government changing its DST rules (it happens several times a year somewhere) silently shifts every future meeting. Keep the rule and the zone; compute UTC when you need it.

3. Invitations: one event, many calendars

Two options:

  • Single copy: the event lives once (in the organizer's calendar); attendees' calendars hold a reference + their RSVP. Edits are instant for everyone, but reading a calendar means joining across shards.
  • Copy per attendee *(common)*: each attendee's calendar gets its own copy, linked by a shared event ID (iCalendar UID). Reads stay single-shard; edits fan out asynchronously via the change stream. External attendees get iCalendar email invites.

Copies carry a version (sequence number), so an attendee's copy applies updates in order and ignores stale ones. Huge invite lists (an all-hands with 50k people) fan out through a queue in batches.

4. Free/busy

"Find a time for these 8 people" needs busy intervals, not event details (privacy!). Maintain a busy index: per user, expanded busy intervals for the next ~90 days, updated from the change stream. A query merges the users' interval lists and returns the gaps — the classic merge-intervals problem. For long-range or open-ended recurrences, expand on demand.

5. Reminders at the right second

Reminders are a job scheduler problem. Don't store one row per future occurrence forever; keep only the next reminder per (event, user) in a time-indexed store, and when it fires, compute and insert the following one from the RRULE.

  • A poller pulls reminders due in the next minute into a delay queue; workers send push/email.
  • Idempotency key (eventId, occurrenceStart, userId, offset) prevents duplicates on retry.
  • Edits and deletes update the next-reminder row via the change stream.

6. Fast week views and sync

  • Cache expanded week views per calendar; invalidate on any change to that calendar.
  • Clients keep a local copy and fetch deltas with a sync token (GET /events?syncToken=… returns changes since the token), the same model CalDAV and Google Calendar's API use. Push a "something changed" notification over a WebSocket or mobile push to trigger the sync.

What's expected at each level

  • Mid-level: events table, sharing/attendees, reading a date range, reminders via a scheduled job.
  • Senior: recurrence rules with exceptions expanded on read, local time + time zone storage, per-attendee copies with versioned fan-out, free/busy via merged intervals.
  • Staff: "this and following" series splitting, DST rule changes, large-invite fan-out, delta sync with tokens, reminder scheduling that scales with infinite series.