InterviewKata
System design · Worked design D43 of 46 · Social and content · Long form

Design a calendar

Design a shared calendar like Google Calendar or Outlook: events, meetings that repeat every week, invitations, finding a time that suits twelve people in four time zones, booking rooms, and reminders that arrive at the right minute. The data is small; the hard parts are time itself (repeating series, time zones, clock changes) and the burst of reminders just before every hour.

Modules this design uses11 Relational data · 17 Contention · 18 Queues · 20 Time · 22 Outbox · Designs D05, D15, D21

1. The prompt

"Design a calendar for a workplace suite: people create events and meetings, invite others, see each other's free and busy times, book rooms and get reminders."

What makes it hard:

2. Frame the problem

How many users, and where?

100 million users across all time zones; the largest companies have 300,000 employees.

Peaks follow the sun: each region's Monday morning is the busiest hour for that region's data.

Do we need repeating events?

Yes: daily, weekly, monthly, "every second Tuesday", with or without an end date, and changes to one occurrence or "this and all following".

That rules out storing occurrences as rows; we need a rule plus exceptions (9.1).

Free/busy for whom?

Anyone in the same company can see when a colleague is busy, not what the meeting is, unless it's shared with them.

Free/busy is its own read model with its own permissions (9.3).

Rooms?

Yes, bookable like attendees; a room must never be double-booked.

A correctness constraint like seats in D15, but on time ranges.

Reminders: how precise?

Within a few seconds of the chosen time (10 minutes before, by default), by push, email or pop-up.

Millions are due in the same minute before each hour (9.5).

Other clients?

Web, phones (offline), and standard calendar apps through CalDAV and iCalendar feeds.

Phones need incremental sync, and the data model should map onto the iCalendar standard (RFC 5545).

3. What it must do

  1. Create and change events, including repeating ones, with exceptions for single occurrences.
  2. Invite people and rooms; they accept or decline; the organiser sees replies.
  3. Show calendars for any date range, and find a time using free/busy.
  4. Remind each attendee at their chosen time before each occurrence.

Not now: video-call links (an external service), tasks and to-dos, AI scheduling assistants, public event discovery.

4. How well it must do it

TargetWhy this number
Week view loads in under 200 ms at p99 speedPeople open their calendar dozens of times a day.
Find a time for 20 people over 2 weeks in under 1 s speedScheduling is interactive.
Reminders delivered within 5 s of their time, even at :50 and :20 load shapeA reminder after the meeting started is useless.
No double-booked rooms, ever consistencyTwo teams at one door is the bug people remember.
Changes visible to other attendees within a few seconds freshnessA moved meeting must not be missed.
99.99% available for reads surviving failuresCompanies run their day from it; reads can be served slightly stale if writes are down.

5. Numbers that change the design

QuantityWorking→ so we…
Event writes100 M users × ~5 changes a week ≈ 70 M a day ≈ 800 a second, a few thousand at peak→ so writes are modest: a sharded relational store, by calendar
Calendar reads100 M × 10 opens a day ≈ 1 B a day ≈ 12,000 a second; ~4× on a region's Monday morning→ so range reads must hit a precomputed index, not expand rules each time
Stored events100 M × ~2,000 events over the years × 2 KB ≈ 400 TB with attendees and history→ so shard by calendar; old years move to cheaper storage
Reminders in one minute40 M users in one region; 1 in 8 has a meeting at 10:00 → 5 M reminders due at 09:50, ≈ 80,000 a second if sent within the minute→ so reminders are scheduled into minute buckets and loaded ahead (9.5)
An all-hands inviteOne event, 300,000 attendees→ so huge meetings are handled like celebrity posts: no per-attendee write fan-out (9.6)

Illustrative numbers.

6. The nouns

event          (calendar_id, event_id) → organizer, title, start_local, end_local, tz ('Europe/London'),
               rrule ('FREQ=WEEKLY;BYDAY=MO'), until?, version, updated_at
exception      (event_id, original_start) → cancelled | new start/end/title
attendee       (event_id, user_or_room_id) → response (needs-action | yes | no | maybe)
occurrence_idx (calendar_id, day_utc) → [(event_id, start_utc, end_utc)]      (materialised 12 months ahead)
busy           (user_id, start_utc, end_utc, event_id)                         (free/busy read model)
reminder       (due_minute, shard) → [(user_id, event_id, occurrence_start, channel)]
change_log     (calendar_id, seq) → event_id, kind                              (sync tokens)

7. The contract

POST  /v1/calendars/{cal}/events              {title, start: {local, tz}, end, rrule?, attendees[], reminders[]} → event
PATCH /v1/events/{id}?scope=one|following|all  {changes, occurrence_start?}   (If-Match: version)
GET   /v1/calendars/{cal}/events?from=…&to=…  → occurrences, in the viewer's time zone
POST  /v1/freebusy                             {users[], rooms[], from, to} → busy ranges per person
POST  /v1/events/{id}/responses                {occurrence_start?, response}
GET   /v1/calendars/{cal}/changes?sync_token=… → changed and deleted events, next token (410 → full resync)

The organiser and responder come from the auth token. If-Match stops two people editing the same meeting from silently overwriting each other (Module 07).

8. First working version

An events service owns events, exceptions and attendees in a relational store sharded by calendar. Every change also writes a row to an outbox (Module 22), from which workers update the read models: the per-calendar occurrence index, the free/busy table, invitations (through the notification system, D05) and the reminder schedule.

8.1 Creating a weekly meeting

8.2 Opening your week

Parked for laterstoring repeating series (9.1) · time zones and clock changes (9.2) · finding a time (9.3) · rooms (9.4) · the reminder peak (9.5) · huge meetings and phone sync (9.6).

9. Make it hold up

9.1 Repeating events: a rule plus exceptions speed · correctness

ApproachWhy
✗ BadA row per occurrence, generated when the series is created"Forever" has no last row; editing the series rewrites thousands of rows; a time-zone rule change means regenerating them all.
~ GoodStore the rule; expand it on every readCorrect and compact, but every week view expands every series the person has, including colleagues' shared calendars.
✓ GreatStore the rule and its exceptions as the truth; keep a derived occurrence index for the next 12 months, extended by a nightly job and rebuilt for a series whenever it changesReads are a range scan; the truth stays small and editable; the index can always be rebuilt from the rule.

Three kinds of edit. This occurrence: write an exception keyed by its original start. All occurrences: change the event and keep the exceptions that still make sense. This and following: end the old series the day before (set its until) and create a new series from that date, so the history before the change stays as it was.

9.2 Time zones and clock changes correctness

Store a repeating event's start as local time plus a time-zone name (09:00, Europe/London), never as a fixed UTC instant. Each occurrence is converted to UTC when expanded, using the IANA time-zone database. A single event in the future also keeps its zone name, because governments change their rules with little notice and the stored UTC instant would then be wrong.

The effect people notice: countries change their clocks on different dates. The US moves in early-to-mid March; the UK and EU at the end of March; south-eastern Australia in April and October; India and Brazil not at all. For those weeks, a meeting fixed in London time moves by an hour for colleagues in New York. That is correct, and the calendar should show it. The lab in section 13 shows it week by week.

9.3 Finding a time for twelve people speed · privacy

The busy table is a read model: one row per occurrence per attendee who accepted or hasn't answered, with no titles. Permission to see free/busy is checked once per person; permission to see details is a separate, stricter check. Working hours and time zones come from each person's settings, so the search only offers slots inside everyone's day.

9.4 Rooms that can't be double-booked consistency

A room is an attendee with its own calendar, and booking it must be atomic. For single events, a database range constraint on the room's bookings (PostgreSQL's exclusion constraint on a time range, as in D15's stays variation) refuses an overlap. For a repeating booking, the service expands the next 12 months, checks every occurrence and inserts them all in one transaction; occurrences beyond that are booked by the nightly job, and a clash found then is reported to the organiser. Popular rooms are contended at round hours, so the booking path is short: no external calls inside the transaction.

9.5 Five million reminders at 09:50 load shape

ApproachWhy
✗ BadA cron job every minute: SELECT … WHERE due = now() across all eventsExpands every series every minute, and one query must return 5 million rows in seconds.
~ GoodA timer per reminder in a job schedulerWorks (D21), but tens of millions of timers fire in the same minute and the scheduler's own peak becomes the problem.
✓ GreatMinute buckets split into shards, loaded a few minutes early, sent by a worker pool sized for the peak; the send is idempotent per (user, occurrence, reminder)The heavy reading happens before the minute; the minute itself is just sending; duplicates after a retry are dropped.

9.6 Huge meetings and phones that sync scale · freshness

9.7 The whole design after the deep dives

10. Interviewer follow-ups

A country abolishes daylight saving time with two months' notice.

Because events store local time plus a zone name, nothing in the source data changes. Ship the new time-zone database, then re-expand the occurrence index and reminder schedule for series in that zone. Single events stored only as UTC would have been silently wrong.

Two people edit the same meeting at once.

Each PATCH carries the version it read (If-Match); the second gets 412 and the client shows the newer version to merge. Responses (accept, decline) are separate rows, so they never conflict with edits.

Invitations to people at another company.

Send an iCalendar invitation by email (an .ics with a unique ID and sequence number); their reply comes back by email and updates the attendee row. The unique ID makes updates and cancellations apply to the right event in their calendar.

Show colleagues' calendars side by side.

Read each person's occurrence index for the range in parallel and filter by permission: details if shared, otherwise just busy blocks. Cache the busy view per person for a minute.

A user deletes their account.

Delete their calendars and events; on meetings they organised, either transfer ownership or cancel and notify attendees; remove them from attendee lists of others' events. The change log tells sync clients to delete local copies.

Reminder sent twice after a worker crash.

The send is keyed by (user, event, occurrence start, reminder offset). The worker records the key before sending (or the push service deduplicates by it), so a retry after a crash finds it and skips.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Index workers lagA new meeting appears in others' calendars lateOutbox ageThe organiser's own view reads the event directly; workers scale out
Reminder shard slow at :50Late reminders for some usersSend delay per bucketBuckets loaded minutes early; spare workers take over shards
Events database shard downThose calendars can't be editedError rate per shardReads served from replicas and the index; writes queue in the client
Time-zone data out of dateMeetings an hour off in one countryVersion check on deployZone names stored, so re-expansion fixes everything
Notification provider downInvites and email reminders delayedProvider errorsRetries with backoff; push and pop-up reminders unaffected (D05)

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: week-view p99 · outbox age · reminder delay at :50 and :20 · room-booking conflicts · sync 410 rate · time-zone database version across servers.

At 10×: more shards; the reminder minute is the first thing to grow, so more shards per bucket and more workers. The model doesn't change.

A 60-minute interview

0–5Frame: repeats, zones, rooms, reminders
5–12Must do, how well, numbers (the reminder minute)
12–18Nouns: rule, exception, occurrence
18–30First version: create a series, open a week
30–52Deep dives: repeats (9.1), zones (9.2), reminders (9.5)
52–60Rooms or free/busy, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including external clients
8–16Must do, how well, numbers
16–24Nouns and contract, edit scopes
24–38First working version
38–78Deep dives 9.1–9.6 and the final picture
78–86Follow-ups: zone rule change, concurrent edits, external invites
86–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelEvents and attendees in a database, recurrence stored as a rule, reminders via a scheduler, range queries for the week view.
SeniorRule plus exceptions with a derived index, local time plus zone name, the three edit scopes, free/busy as a read model, room bookings enforced by the database, the reminder peak.
Staff+Time-zone rule changes and re-expansion, huge meetings vs fan-out, sync tokens and standards (iCalendar, CalDAV), privacy of free/busy, operating the nightly extension job.

Common mistakes:

13. Try it, check yourself, read more

The clock-change lab. A weekly meeting is created by an organiser in one city. Pick where they are, the meeting time, and where a colleague is. Then compare storing it as local time plus a zone name with storing a fixed UTC time.

Organiser inatevery Monday
Colleague in
Stored as

Computed in your browser with its own time-zone database, for Mondays from 2 March to 4 May 2026.

How should a weekly meeting with no end date be stored?

The rule is the truth; the index makes reads fast and can always be rebuilt.

What should a repeating 9:00 meeting store as its start?

UTC or a fixed offset drifts by an hour when the clocks change; a zone name follows the rules, including future changes.

A user changes "this and all following" occurrences of a weekly meeting. What happens to the data?

History stays as it happened, and the future gets its own rule.

5 million reminders are due at 09:50. What keeps them on time?

The reading happens before the minute; the minute is only sending.
ResourceWhy
RFC 5545: iCalendarThe standard for events, repeat rules (RRULE) and exceptions (RECURRENCE-ID, EXDATE).
RFC 4791: CalDAVHow calendar apps sync with a server.
IANA time zone databaseThe source of every zone's rules and their changes.
Google Calendar API: synchronize resources efficientlyA product's version of sync tokens, including the 410 full-resync rule.
Microsoft Graph delta queriesThe same idea in Outlook's API.