Design Google Calendar — System Design Interview Guide
Design Google Calendar is a system-design interview that asks you to build a calendar system: users create events, invite others, see free/busy across teammates, get reminders, and handle recurring events. The hard part is recurrence expansion plus cross-user free/busy queries at organization scale.
By Alex Chen, Founder, InterviewChamp.AI · Last verified
Reported in interviews at
- Microsoft
- Apple
- Atlassian
- Meta
Sourced from Glassdoor, Levels.fyi, and Blind interview reports.
Functional requirements
- Create an event with start/end time, title, location, description, and attendees
- Invite other users; they receive a notification and can accept/decline/maybe
- Recurring events (daily, weekly, monthly, custom RRULE) with edit-this/edit-future/edit-all options
- Free/busy lookup across multiple users to find a time everyone is available
- Reminders via push notification, email, or in-app at user-defined intervals before the event
Non-functional requirements
- Calendar load: <300ms p99 to render a full month view of the user's calendar (typically ~100-200 events)
- Reminder delivery: ±10 seconds of the scheduled time
- Scale: ~3B+ users (Google Workspace + consumer), peak ~50K events/sec, ~1B+ reminders/day
- Availability: 99.99% for reads; missed reminders are bad UX but not catastrophic
Capacity estimation
Google Calendar public scale (2023-2024): part of Google Workspace + consumer Gmail accounts, so ~3B+ users have a calendar enabled. Active calendar users are a fraction of that — likely several hundred million daily. Average user creates a few events per week; the long tail (executive assistants, recruiters, healthcare schedulers) creates hundreds per week.
Storage: an event row is ~1-2 KB (title, times, attendee list, RRULE, metadata). At ~10 events/user/week average × 1B active users × 52 weeks = ~500B events/year created. Most events are short-lived (one-off meetings); retention is typically forever. Total event-row storage: ~1 PB before indexes.
Reminder volume: ~1B+ reminders/day = ~12K reminders/sec average, with significant spikes at top-of-hour and quarter-past-hour boundaries (most meetings start on common offsets). Peak reminder rate can reach ~100K/sec at common meeting times.
The shape that matters: this is a time-indexed problem. Every read is 'events between time T1 and T2'; every reminder is 'fire at time T'. Time-range indexing is the central data-modeling decision. Recurring events add a complication — a 'weekly standup' is one row but expands to hundreds of occurrences. Expanding all recurrences eagerly bloats storage; expanding on read is fast for one user but slow for cross-user free/busy queries.
High-level design
Four core domains: event storage, free/busy index, reminder service, and notifications.
Event storage: a sharded relational store keyed by owner_user_id holds event rows. Each row has (event_id, owner_id, start_time, end_time, title, location, description, attendee_list, rrule, exceptions). Recurring events store a base row + an exceptions list (skipped occurrences, edited occurrences). The calendar view for a user is a range query: 'all events where owner_id = me OR I'm in attendee_list, between start_time and start_time + 30 days'.
For recurring events, the storage stores the rule (RRULE in iCalendar format: 'FREQ=WEEKLY;BYDAY=MO,WE,FR'). Expansion to concrete occurrences happens at read time within the requested time window. For a 1-year view of a weekly standup, expansion produces ~52 occurrences in microseconds. The exceptions list overrides the rule for specific dates (a moved or canceled occurrence).
Free/busy index: a separate read-optimized index built per user, storing only (start_time, end_time) tuples — no metadata. Free/busy queries ('when are users A, B, C, D all free between 9am-5pm?') hit this index for each user, intersect the busy intervals, and return free windows. The index is denormalized from the event store via change-data-capture, so writes are slightly delayed (~1-5 seconds) but reads are fast.
Reminder service: when an event is created or updated, a reminder entry is scheduled at (event.start_time - reminder.lead_time) in a time-ordered queue (delay queue or priority queue keyed by trigger_time). A worker pool consumes entries as they become due and dispatches notifications. The queue is partitioned across many workers; each worker pulls entries with trigger_time <= now and processes them. Reminders for recurring events are scheduled per-occurrence at expansion time.
Notifications: dispatch goes through three channels — push notification (mobile), email (transactional), and in-app banner. Each channel has its own delivery service with retries. Idempotency is per (user_id, event_id, occurrence_time, channel) so a duplicate reminder fire doesn't double-notify.
Deep dive — the hard problem
Two deep dives: recurring events and cross-user free/busy.
Recurring events: the iCalendar RRULE standard expresses recurrence rules compactly (FREQ, INTERVAL, BYDAY, BYMONTHDAY, COUNT, UNTIL). Storing the rule is small; expanding it to concrete occurrences is computational. Two implementation choices.
Eager expansion: when a recurring event is created, materialize each future occurrence as a separate event row up to a horizon (e.g. 1 year out). Pros: reads are simple range queries with no expansion logic. Cons: storage bloat (a daily event for 5 years = 1,800 rows), edit complexity (editing the rule requires updating all materialized rows), exceptions are first-class but every occurrence is a row.
Lazy expansion: store only the rule + exceptions list, expand at read time. Pros: minimal storage, simple edits (one rule update), exceptions list cleanly separates 'this occurrence is different' from 'all occurrences are different'. Cons: read-time CPU cost (small but non-zero), and cross-user free/busy intersection requires expanding each user's recurring events into the query window.
Production calendars use lazy expansion as the default with a denormalized free/busy index for query speed. Mention both options, pick lazy with a reason. Edit semantics: when a user 'edits this occurrence', that becomes a single exception row; 'edit future occurrences' splits the original rule into two (the old one ends, a new one begins on the edit date); 'edit all' updates the original rule.
Free/busy: querying 'when are users A, B, C all free between 9am-5pm on Tuesday' requires fetching each user's busy intervals in that window and intersecting. The naïve cross-shard approach fans out to each user's home shard, fetches their busy intervals (including expanded recurrences), and intersects centrally. For an org of 10 attendees, this is 10 small fan-out queries — fine. For a meeting-finding tool that scans 200 attendees, this is too many queries.
The standard optimization: a per-org or per-team aggregated free/busy index that pre-computes the union of multiple users' busy intervals at indexing time, suitable for 'find a 1-hour slot when 200 people are all free'. This shifts cost from query time to write time, acceptable because organizational meetings are scheduled less often than they're rendered. Mention the tradeoff between live fan-out and pre-aggregated views.
Third hard problem: timezones and DST. An event 'every Monday at 9am' is in the creator's timezone; an attendee in a different timezone sees a different local clock time. The standard answer: store events in UTC + the creator's timezone identifier (e.g. America/Los_Angeles). Render conversion happens client-side. Daylight Saving Time transitions are handled by the timezone library; never store wall-clock times without a timezone.
Common mistakes
- Eagerly materializing every recurring occurrence — storage and edit-complexity blow up
- Storing events in local wall-clock time without a timezone — DST and travel break everything
- Designing free/busy as a live cross-user table scan — fan-out is unbounded at meeting-finder scale
- Forgetting the reminder service — interviewer always asks 'how do you fire 1B reminders a day'
- Treating attendees as a foreign-key join to the user table — at scale, denormalize the attendee list into the event row
Likely follow-up questions
- How would you support a 'find a meeting time when these 50 people are free' query in <500ms?
- What changes if the system has to handle organization-wide events (10K-person all-hands)?
- How would you implement an iCalendar import/export for external calendar interop?
- How would you handle a daily event scheduled until year 2199 (a long-running recurrence)?
- How would you add a 'smart scheduling' feature that suggests meeting times based on focus-time preferences?
Practice Design Google Calendar live with an AI interviewer
Free, no sign-up required. Get real-time feedback on your design.
Practice these liveFrequently asked questions
- How long is the Design Calendar interview at Google or Microsoft?
- 60 minutes is the norm at L5+/Senior+. The expectation is recurrence + free/busy + reminders + timezones covered explicitly. Source: Glassdoor Google/Microsoft 2022-2024 reports.
- Do I need to know RRULE syntax exactly?
- Naming RRULE as the iCalendar recurrence-rule standard and being able to give one example (FREQ=WEEKLY;BYDAY=MO) is enough. Reciting the full BNF is overkill.
- Is Design Calendar easier than Design Slack?
- Comparable. Calendar's hard surface is time-indexed data + recurrence; Slack's is real-time chat + multi-tenancy + search. Most interviewers consider them roughly equal in difficulty at the senior level.
- Should I cover natural-language event creation ('lunch tomorrow at noon')?
- Briefly mention it as an NLP layer in front of the event-creation API. Drilling into the parser is bonus signal only if you've covered the core design — recurrence, free/busy, reminders, timezones — first.