The problem
"Design calendar invites." Ana creates a meeting and invites people; each guest accepts, declines or says maybe, and sees the meeting in their calendar. Meetings can repeat ("every Tuesday at 10:00"), one meeting of a series can be moved, and every guest gets a reminder 10 minutes before. Park finding a time when everyone is free and booking rooms: each is a design of its own.
This practice problem reuses the pattern of Design a Job Scheduler for the reminders and the repeating schedule, and sends invites through the Notification System. What's new: one event with many guests and their answers, any future week on screen, and changes to one meeting of a series.
The numbers
From the interviewer: 50 million events created a day, about 6 guests each. Round a day to 100,000 seconds (86,400 really).
- Invitations: 50 million × 6 = 300 million a day, about 3,000 a second (about 3,500 with 86,400), each an email or a push. Every change to an event sends one per guest again.
- Attendee rows: one per invitation, 300 million new rows a day; at about 500 bytes each, 150 GB a day.
Guests and answers: an attendee row each
record fields
event id, title, start, end, zone, rule, organizer, room
attendee event_id, user_id, answer, note, new time
exception event_id, replaces, new start or canceled
One event row holds what every guest shares: the title, the time, the organizer, the room. An attendee row per guest holds what's theirs: the answer, a note such as "I'm away that day", a new time the guest suggests, such as 15:30. It's the many-to-many join of Data Modeling, with the answer on the join row.
When 200 people answer the all-hands, they write 200 different rows. A guest list inside the event row would make every answer rewrite the same row, and two answers at once could lose one; a full copy of the event per guest would make moving the meeting 200 writes. A guest's calendar is a lookup of their attendee rows, indexed by user.
Repeating events: store the rule
Ana's team standup is every Tuesday at 10:00 from Oct 27, with no end date. The event row stores the rule, often written as an iCalendar RRULE such as FREQ=WEEKLY;BYDAY=TU, with its time zone, as in Reminders. When someone opens the week of Nov 9, the API works out the Tuesdays in it: Nov 10.
A row per meeting can't hold a series with no end, and even 10 years is about 520 rows, 6,240 attendee rows with 12 guests, all rewritten when the standup moves to 9:30. Keeping only the next meeting, like the Job Scheduler's next_run_at, can't show next month. Some systems keep the next few weeks expanded as a cache, but the rule stays the source.
One meeting or the whole series
Ana moves only the Nov 10 standup to Wednesday, Nov 11. That's an exception row: series e7, the meeting it replaces (Tue Nov 10, 10:00) and its new time. Showing a week expands the rule, then applies the exceptions:
week of from the rule shown
Nov 2 Tue Nov 3, 10:00 Tue Nov 3, 10:00
Nov 9 Tue Nov 10, 10:00 Wed Nov 11, 10:00 (moved)
Nov 16 Tue Nov 17, 10:00 Tue Nov 17, 10:00
A canceled meeting is an exception marked canceled, and keying it by the date it replaces means moving it twice finds the same row. The three edits a calendar offers each touch something different:
- Only this meeting: one exception row.
- All meetings: the event row's rule or title.
- This and all following: end the old rule before that date and start a new series from it.
Answers can have exceptions too: Ben can decline just the Nov 10 standup, kept next to his answer to the series.
Reminders for every guest
Scheduling a reminder per guest per meeting fails for a series with no end, and even one year of the 12-person standup is 624 reminders to find and move when it changes. Expanding every rule every minute to find meetings starting soon reads the whole database for a few hits.
Instead it's the Job Scheduler's pattern: one job per event, with a next_run_at 10 minutes before its next meeting, found through an index (Indexes). When it runs at 09:50 it reads the attendee rows, skips anyone who declined or was removed, hands one alert per guest to the Notification System, and works out the next meeting from the rule and its exceptions. After Nov 3 that's the moved meeting, so the next run is Wed Nov 11 at 09:50.
Same pattern as the Job Scheduler
A reminder job is a job: a due time in an index, a unique key on (event, meeting time) so a scheduler takeover doesn't alert twice, and alerts that are safe to repeat. Invitations and updates go to the Notification System keyed by event, change and guest, so a retry never reaches a guest twice. The API:
POST /events {: "Team standup", : "2026-10-27T10:00",
: "FREQ=WEEKLY;BYDAY=TU", : […]}
→ 201 Created {: "e7"}
POST /events/e7/answer {: "accepted"}
GET /calendar?week=2026-11-09
→ 200 OK [{: "e7", : "2026-11-11T10:00", : true}]
Summary
- Scope: invites with answers, repeats, one-off changes, reminders.
- Data: one event row; an attendee row per guest with their answer.
- Numbers: about 3,000 invitations a second.
- Services: the Notification System sends invites and changes.
- Repeats: a rule on the event, expanded when a week is shown.
- One meeting or all: an exception row per changed meeting.
- Reminders: one job per event on next_run_at; guests read when it runs.