Design problems
Booking & traffic spikesHardSDE-2 40 min

Design a calendar with invites (Google Calendar)

Asked at
AppleAtlassianGoogleMicrosoftSalesforceZoho

The interviewer asks

“Design a calendar service like Google Calendar. Users create one-off and recurring events, invite other people (inside and outside the company), see each other's free/busy time, get reminders, and see changes on all their devices.”

What a strong answer looks like

A strong answer gets the data model right first: an event is one shared object owned by the organiser, with an attendee list carrying each person's RSVP, and recurring events are stored as a rule (RRULE) plus exceptions, expanded into instances only when a time window is read. Times are stored with the IANA time zone of the event, because a 9 am weekly meeting must stay at 9 am across daylight-saving changes. Each user's calendar view is a per-user index of event references for fast range queries, built on write. Invites fan out to attendees' calendars and to external people by email using iCalendar (iMIP). Reminders are a scheduled-job problem: millions of timers per minute, driven by a time-bucketed queue. Devices sync incrementally with sync tokens over a change log. Free/busy is a range query over the per-user index, precomputed into busy intervals. Deep dives: recurring events and exceptions, time zones, the reminder scheduler, and sync.

Concepts to mention

single authoritative event with attendee listRRULE plus exceptions, expanded at read timewall-clock time with IANA time zoneper-calendar index for range queriesfan-out on write for attendeestime-bucketed reminder scheduler with version checksync tokens over a change logetag / If-Match optimistic concurrencyiCalendar / iMIP for external invitesfree/busy as cached busy intervals

1 Requirements

Functional

  • Create, edit and delete events: title, time, time zone, location, description, video link, attachments.
  • Recurring events (daily, weekly on Mon/Wed, monthly on the last Friday) with edits to one instance, this-and-following, or all.
  • Invite attendees by email; they RSVP (yes/no/maybe); organiser sees responses; external attendees get email invites.
  • View day/week/month for one or several calendars; share calendars with permissions (free/busy only, read, edit).
  • Free/busy lookup for a group to find a meeting time; room and resource booking.
  • Reminders by push, email or SMS at configured offsets.
  • Sync across web, mobile and third-party clients (CalDAV / API) with offline edits.

Non-functional

  • Scale: ~500M users, ~100M daily active; ~1B events created per month including instances changes.
  • Week view load p95 under 200 ms; free/busy for 10 people under 300 ms.
  • Reminders fire within ~30 seconds of their time; never twice, never for cancelled events.
  • Changes visible on other devices and to attendees within a few seconds.
  • Strong consistency for an organiser's own edits; eventual (seconds) for attendee copies.
  • Availability 99.99%; data durable across regions.

Out of scope (say it out loud)

  • Smart scheduling suggestions with ML (mention only).
  • Video meeting infrastructure.
  • Email system itself (assume an email service).

2 Estimates

Calendar reads~100k/s peak100M DAU × ~20 views/day (app opens, widget refresh, sync) ≈ 2B/day ≈ 23k/s avg; ×4 at 9-10 am peaks across time zones.
Event writes~5k/s peak~100M writes/day (creates, edits, RSVPs) ≈ 1.2k/s avg; ×4 peak.
Invite fan-out~5 attendees avg, up to 10k+ for all-handsEach write updates ~5 attendees' indexes ⇒ ~25k index writes/s at peak.
Reminders~300k fire per minute at peakMeetings cluster at :00 and :30; ~1B reminders/day with heavy spikes 10 and 5 minutes before the hour.
Events stored~200B event rows over time, ~100 TB500M users × ~400 events/year × years of history; ~1-2 KB per event before replication. Recurring series stored once.
Sync requests~50k/sDevices sync on push notification or every few minutes; most return 'no changes'.

3 API

EndpointWhat it does
POST/v1/calendars/{cal_id}/eventsCreate {title, start, end, tz, rrule?, attendees[], reminders[]}; Idempotency-Key.
PATCH/v1/events/{id}?scope=this|following|all&instance=Edit an event or a recurring instance; If-Match etag for optimistic concurrency.
GET/v1/calendars/{cal_id}/events?from=&to=Expanded instances in a window for the week/month view.
POST/v1/events/{id}/rsvpAttendee responds {status, comment}.
POST/v1/freebusy{emails[], from, to} → busy intervals per person (respecting sharing permissions).
GET/v1/calendars/{cal_id}/changes?sync_token=Incremental sync: changed and deleted events since the token, plus a new token.
WS/v1/push (or mobile push channel)Lightweight 'calendar changed' pings that trigger a sync.

4 Data model

eventsSpanner / CockroachDB or sharded MySQL (by organiser calendar)
event_idcalendar_id (owner)titlestartendtzrruleexdatesrecurring_parent_idoriginal_start (for exceptions)visibilityversion (etag)updated_at

One authoritative copy, owned by the organiser's calendar. Exceptions are rows pointing to the series with original_start.

attendeesSame DB, co-located with event
event_idattendee (user_id or email)rolersvprsvp_atis_external

RSVP updates the attendee row only, not the whole event.

calendar_indexBigtable / Cassandra (partition by calendar_id, clustered by time bucket)
calendar_idmonth_bucketevent_idstartendis_recurringrrule summaryrsvpbusy flag

Per-user (per-calendar) view of events they own or attend, written on fan-out. Recurring series stored once with their range; expanded at read time.

change_logSame store as index
calendar_idseq (monotonic)event_idchange_type

Drives sync tokens: a token encodes the last seq the client saw.

remindersTime-bucketed queue (Bigtable/Cassandra rows by minute) + workers
fire_minute (partition)shardreminder_iduser_idevent_idinstance_startchannelevent_version

Only the next occurrence per series is scheduled; the next one is scheduled after firing.

freebusy cacheRedis
fb:{calendar_id}:{week} → busy interval listversion

Recomputed on change; makes group lookups cheap.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALCalDAV / ICSwritesreads, synctxn + outboxchangesattendee indexinvites(re)schedulefire reminderevent detailson missWeb / mobile clientsCalDAV / other serversAPI gatewayEvent serviceCalendar view + syncFree/busy serviceKafka (event changes)Invite fan-out workersReminder schedulerEvents DB (Spanner)Calendar index + logRedis (free/busy)Push / email (iMIP)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Create a weekly meeting with 6 attendees

  1. 1Organiser creates 'Team sync, Mondays 10:00 Asia/Kolkata, RRULE:FREQ=WEEKLY;BYDAY=MO' with 6 attendees.
  2. 2Event service writes the event row, 6 attendee rows and an outbox entry in one transaction on the organiser's shard; returns event_id and etag.
  3. 3Fan-out workers add the series to each internal attendee's calendar index (one row per series, not per instance) and append to each calendar's change log.
  4. 4They send push 'calendar changed' pings to attendees' devices and email iMIP invites (ICS attachment) to external attendees.
  5. 5The reminder scheduler computes the next occurrence's reminder time per attendee and inserts it into the right minute bucket.

Open the week view

  1. 1Client calls GET events?from=Mon&to=Sun; the view service reads the calendar index partitions covering that week, including recurring series whose range overlaps.
  2. 2It expands each RRULE into instances in the event's time zone, applies exceptions (moved or cancelled instances), and converts to the viewer's time zone.
  3. 3It fetches event details for visible items (batch read, cached) and returns instances with the user's RSVP state.

Attendee declines one occurrence

  1. 1Attendee sets 'No' for the 3 March instance; the app sends POST /events/{id}/rsvp with the instance's original_start.
  2. 2The event service stores an attendee-level exception for that instance (RSVP keyed by original_start) without touching the series or other attendees.
  3. 3Organiser's view updates via the change log; the reminder for that instance for this attendee is cancelled by version check when it fires.

7 Deep dives

Recurring events: rule, exceptions and expansion

Never store every instance of a recurring event: a daily meeting with no end date is infinite, and editing 'all future' would rewrite thousands of rows. Store the series once with an RFC 5545 RRULE (FREQ, INTERVAL, BYDAY, COUNT/UNTIL), a start in its time zone, and a list of EXDATEs. A change to one instance creates an exception row linked to the series with original_start identifying which occurrence it replaces; a cancelled instance is an EXDATE. 'This and following' splits the series: the old one gets UNTIL just before the chosen instance and a new series starts from it, copying attendees. Reads expand the rule only over the requested window, which is cheap with a good RRULE library, then overlay exceptions. Indexing: the calendar index holds the series with its overall range (first start to UNTIL, or open-ended), so a week query finds it even without stored instances. Edge cases that interviewers like: the 31st of the month in February (RFC rules skip it), the last Friday (BYDAY=-1FR), and RSVPs per instance, which are attendee-level exceptions.

Time zones and daylight saving

Store recurring events in their wall-clock time plus an IANA time zone (America/New_York), not as UTC. A weekly 9:00 meeting in New York is 13:00 UTC in summer and 14:00 UTC in winter; storing UTC would shift it by an hour after the DST change. Expansion happens in the event's zone, then each instance is converted to UTC for indexing and to the viewer's zone for display. One-off events can be stored as UTC plus the original zone for display. India has no DST, which hides these bugs during local testing, but teams in India meet with US and EU colleagues daily, and a meeting with London moves for Indian attendees twice a year: that is correct behaviour and the product should show it. Time zone rules themselves change by government decision, so ship tzdata updates regularly and recompute affected future instances and reminders. All-day events are dates, not times; store them as dates with no zone, or a birthday shows on the wrong day for a traveller.

Reminder scheduler at 300k fires per minute

Reminders are timers: billions of them, spiky around :50 and :55 every hour. Use a time-bucketed store: rows keyed by fire minute and a shard number (to spread a hot minute across many partitions), each holding reminder IDs. Workers own shards via leases; a few seconds before each minute they read their bucket, and for each reminder check the event's current version and the attendee's RSVP before sending, so edits and cancellations need no cleanup of old reminders: stale ones are dropped at fire time. Schedule only the next occurrence of a recurring series; after firing, compute and insert the next one. This keeps the store small and makes rule edits cheap. Send through the notification service with an idempotency key (reminder_id + instance_start) so a worker retry after a crash does not send twice. Late is better than never: if workers fall behind, send reminders up to a few minutes late, and drop those for meetings that already started. Clients also schedule local notifications from synced data, which covers offline phones and acts as a backup.

Multi-device sync with sync tokens

Clients must show the same calendar everywhere and work offline. Each calendar has an append-only change log with a monotonic sequence number per calendar. A client sends its last sync token (encoding the sequence) and gets all events changed or deleted since then, plus a new token; if the token is too old (the log was compacted), the server returns 410 Gone and the client does a full resync of the window. Push notifications only say 'calendar X changed', which is cheap and triggers a sync, so you never push full payloads to millions of devices. Offline edits are sent later with the event's etag (If-Match); if the event changed meanwhile, the server returns 412 and the client merges field by field or asks the user. RSVPs are separate attendee resources so they rarely conflict with organiser edits. Third-party clients use CalDAV, which has the same idea (sync-collection reports with tokens), and external organisers' invites arrive as iMIP emails that the system parses into events with the external UID.

8 Trade-offs

One shared event vs a copy per attendee

A copy per attendee (like email) makes reads local and simple, but edits must update every copy and copies drift. One authoritative event owned by the organiser plus a per-attendee index (reference + personal state like RSVP and reminders) keeps one truth and cheap reads. External attendees necessarily get copies via iMIP, which is why updates to them are sent as new emails with a sequence number.

Expand recurrences at read time vs materialise instances

Read-time expansion keeps storage small and edits cheap; it costs a little CPU per view. Materialising a limited horizon (e.g. next 12 months) speeds up some queries like free/busy across many people but requires re-materialising on edits. Expand at read time, and cache free/busy intervals per week.

Global database vs regional shards

Invites cross users in different regions, and the organiser's write must be consistent. A globally consistent database (Spanner) simplifies this at higher write latency; regional shards with async fan-out are cheaper but make cross-region invites eventually consistent. Most products accept seconds of delay for attendee copies.

9 Bottlenecks

All-hands with 20,000 attendees

Fan-out to 20k calendar indexes and 20k emails per edit is heavy, and every edit repeats it. Treat large events like groups: attendees reference a group, the index stores one entry per group membership expanded lazily, RSVPs are counted asynchronously, and edits to large events are rate limited.

Top-of-the-hour reminder spikes

Reminder volume at :55 can be 20x the average. Spread hot minutes across many shards, pre-read buckets a few seconds early, and scale notification senders ahead of known peaks (weekday mornings across time zones).

10 Failure handling

Fan-out workers lag

The organiser sees the change immediately (strong write), attendees a little later. Kafka retains events, so nothing is lost; workers are idempotent (upsert index rows by event_id) so replays are safe. Alert on lag; clients can fetch the event directly when opened.

Reminder shard owner dies

Its lease expires in ~10 seconds and another worker takes the shard, processing the current and any missed buckets. Idempotency keys stop duplicates if the old worker had partially sent a bucket.

Bad tzdata deploy

Wrong time zone rules shift meetings. Version tzdata, roll out gradually, and keep the ability to recompute instances and reminders from stored wall-clock times and zone names, which is only possible because you did not store recurring events as UTC.

11 What impresses

  • Refuses to materialise infinite recurrences and explains this-and-following splits.
  • Explains why recurring events need wall-clock time plus zone, with a DST example.
  • Schedules only the next reminder per series and validates version at fire time.
  • Uses sync tokens and push pings instead of pushing full data or polling everything.
  • Treats all-day events as dates, not instants.
  • Knows the standards: RFC 5545 iCalendar, iMIP email invites, CalDAV.

12 Mistakes

  • Storing every instance of a recurring event.
  • Storing everything in UTC and breaking meetings after DST changes.
  • A cron job that scans all events every minute for reminders.
  • Copying the full event to every attendee and updating all copies synchronously.
  • Ignoring external attendees and email invites.
  • No concurrency control for edits from two devices.

13 Follow-ups

Q1How do you implement 'find a time' for 8 people across 3 time zones?
Q2How would you book meeting rooms so two meetings never get the same room?
Q3What happens when the organiser leaves the company?
Q4How do you handle an external organiser updating an invite by email?
Q5How do you migrate a user with 50,000 events from another calendar provider?
Q6How do you support delegation (an assistant managing someone's calendar)?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free