The 100 problems, solved / Collaboration and productivity / 054 · Design Google Calendar

Design Google Calendar

Events, invitations, recurring rules and conflict detection across time zones.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional

  • Create, edit and delete events with a time, a time zone, attendees and a location; invite others and track responses.
  • Recurring events (every weekday, the second Tuesday of the month) with exceptions for single occurrences.
  • Detect conflicts for an attendee and suggest free slots across several people's calendars.
  • Reminders by push and email; changes appear on every device within seconds.
  • Out of scope: meeting rooms booking policies, video links, external CalDAV sync.

Non-functional

  • Scale: 1 billion users, 2 billion events created a year, a typical user views a week with 20 events; peak 100,000 event writes a second on Monday morning.
  • Latency: load a week view under 200 ms; conflict check under 300 ms for 10 attendees.
  • Consistency: a user never sees their own write disappear; an invitation reaches every attendee exactly once.
  • Correctness: times are right across daylight-saving changes and for recurring rules years out.
Back of the envelope

2 × 109 events/year ÷ 3.15 × 107 s ≈ 65 writes/s average, 100,000/s at peak (invites fan out to attendees)

Week views: 300 million daily actives × 5 views = 1.5 × 109 reads/day ≈ 17,000 reads/s, 100,000/s at peak

Storage: 2 × 109 × 1 KB × 10 years = 20 TB of events, plus an attendee index of similar size

Reads dominate and are always a time range for one calendar, which drives the storage layout.

2. Core entities

  • Calendar: calendar id, owner, time zone, sharing ACL.
  • Event: event id, organiser calendar, title, start and end as UTC instants plus the original time zone, recurrence rule (RRULE), version.
  • Occurrence exception: event id, original occurrence date, override (moved, cancelled).
  • Attendance: event id, attendee calendar id, response (accepted, declined, tentative, none), notification state.
  • Reminder: event id, user, fire at, channel.

3. API

  • GET /calendars/{id}/events?from=&to=&tz= returns occurrences in the range, with recurrences expanded; the main read.
  • POST /events with times, zone, attendees, rule; returns the event and the fan-out status.
  • PATCH /events/{id}?scope=this|following|all for edits to recurring series.
  • POST /events/{id}/respond with accept, decline or tentative.
  • POST /freebusy with attendee ids and a range; returns busy intervals and suggested free slots.

4. High-level design

ClientsEdgeServicesDatawriteevent changedattendee copiesbusy rangesWeb / mobileAPI gatewayEvent serviceFree/busy serviceInvite fan-out workerReminder schedulerEvents DBsharded by calendarEvent busWeek-view cachePush / email
  1. The organiser creates an event. The event service writes it to the organiser's shard, computes the first few occurrences, and publishes an event-changed message.
  2. The fan-out worker writes an attendance row into each attendee's shard (so each attendee's week view is a single-shard read) and sends invitations through push and email.
  3. A week view reads the calendar's shard for events overlapping the range, expands recurrence rules in the requested zone, applies exceptions, and caches the result keyed by calendar and week with a version tag.
  4. The reminder scheduler keeps a time-ordered queue of upcoming reminders and fires them through the same notification path.

5. Deep dives

How are recurring events stored and queried without materialising forever?

Store the rule, not the occurrences. A "every weekday for ever" event must not become millions of rows. Materialise occurrences for a rolling window, say the next 12 months, into an occurrence index keyed by calendar and date, and extend the window with a nightly job; beyond the window the week view expands the rule on the fly, which is cheap for one event. Exceptions live in their own table keyed by the original occurrence date, and the reader overlays them: a moved occurrence appears at the new time and the original date is suppressed. Edits with scope "this and following" split the series: the old series gets an end date and a new series starts, keeping ids stable for attendees who already responded to earlier occurrences.

What is the storage layout for fast week views and conflict checks?

Reads are a range by calendar and time, so the events table is sharded by calendar id and sorted by start time within a shard. The attendee copy written by the fan-out worker is the key trick: a user's view never joins across other people's shards. Conflict detection for ten attendees is then ten single-shard range reads of busy intervals, done in parallel, merged into a sorted list of busy periods, and the gaps are the free slots; that is a 300 ms budget comfortably met. Keep the busy intervals as a compact per-user structure (start, end, privacy flag) separate from event details so the free/busy service does not read titles it is not allowed to see.

How do time zones and daylight saving stay correct?

Store the organiser's wall-clock time and the IANA zone, and derive UTC instants at materialisation time with the current zone database. A 9:00 weekly meeting in Hyderabad stays at 9:00 local when New York changes its clocks, and the occurrence index is regenerated for the window whenever the zone database updates (a few times a year). All-day events are dates without zones and must never be converted. Attendee views convert instants to the viewer's zone at read time. Ordering and clocks matter again in the reminder scheduler, which must fire on UTC instants recomputed after a zone change.

How does an invitation update reach attendees exactly once and stay consistent?

The organiser write and the attendee copies are in different shards, so this is a cross-shard update. Rather than a two-phase commit on every edit, the event write commits locally with a version number, publishes to the bus with that version, and the fan-out worker applies it to each attendee with an idempotent upsert keyed by event id and version; replays and retries are harmless, and an attendee copy never goes backwards. An attendee who opens the invite before the copy lands sees the organiser's record through a fallback read, so the user-facing promise holds while the copies catch up within seconds.

How are reminders fired on time for a billion users?

Reminders are a time-ordered queue of (fire at, user, event). Keep the next 24 hours in a sorted store partitioned by minute buckets; a scheduler fleet claims a bucket with a short lease, sends the reminders inside it, and marks them done, so a crashed scheduler's bucket is picked up by another within a minute. Reminders further out are regenerated nightly from the occurrence index, which is also how an edit to a recurring event reaches its future reminders: the old ones are invalidated by event version, and sending checks the version before firing. The whole path is at-least-once with an idempotency key of event id, occurrence date and user, so a retry never pings twice.

6. What a strong answer sounds like

  • State the read pattern first: a range by calendar; everything in the storage layout follows from it.
  • Explain the rule-plus-window approach to recurrence, and the "this and following" split.
  • Be explicit about wall-clock plus zone storage and regeneration on zone-database updates; interviewers like the daylight-saving example.
  • Describe fan-out to attendee shards with versioned idempotent upserts instead of a distributed transaction, and say why.
  • Expect follow-ups on room booking (a room is a calendar with a strict no-overlap rule, enforced by a per-room lock), external invitees by email (iCalendar messages with sequence numbers), and very large meetings with 10,000 attendees (fan-out in batches, attendance stored as a separate wide table).

02Where this question was reported