The Forward Deployed

OpenAI Interview: Design Google Calendar

A full solution to the OpenAI calendar question: the event model, time zones and daylight saving, recurring events with exceptions, expand-on-read versus materialized instances, fast range queries per view, invitations, sync tokens across devices, conflicts, and reminders.

By Reviewed

Part of the OpenAI system design question bank. The question is representative of the round. The analysis and solution are this site's own.

Problem statement

Users own one or more calendars. They create single and recurring events, invite people, and respond to invitations. The app shows day, week, month, and year views, on the web and on phones that sync in the background. Room booking, free-busy search across companies, and task lists are out of scope unless added.

The two hard parts the question names: fast retrieval for each view, and near-real-time propagation of changes across devices. Both depend on the data model.

Clarifying questions

  • Scale? For practice: 500 million users, 5 new events per user per week.
  • Recurring events? Yes: daily, weekly, monthly rules, with end dates or no end.
  • Invitations? Yes, with responses: accepted, declined, tentative.
  • Time zones? Users travel; events are created in a zone and must show correctly anywhere.
  • Offline edits on phones? Yes, synced later.
  • Reminders? Notifications before events, per user.

What makes a calendar hard

A calendar looks like a table of events with start and end times. Three things break that picture.

Recurring events are infinite. A weekly meeting with no end has an instance every week forever. You cannot store infinite rows, but views must show instances as if they were rows, and users edit single instances: "move just this week's meeting."

Time is not a number. A weekly 9:00 meeting in New York happens at a different UTC time in summer and winter because of daylight saving. Store it as UTC alone and it drifts by an hour twice a year.

And every change must reach many devices. One edit by an organizer changes the event on every attendee's calendar, and every one of their phones and laptops must learn about it within seconds, without re-downloading whole calendars.

So the driving tension is a compact, correct model versus fast reads. Storing rules keeps the model small and correct; views want flat rows in a range.

flowchart LR
  E[(Events:<br/>rules + exceptions)]:::store --> X[Expand into instances]:::svc
  X --> V[View: range query<br/>week of Sep 22]:::svc
  CH[(Change log per calendar)]:::store --> SY[Sync to devices]:::svc
  E --> CH
  classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717;
  classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;
Key idea. Store the rule as the truth, serve views from instances, and sync devices from a change log.

Key concepts

Recurrence rules

The iCalendar standard describes repetition as a rule, such as FREQ=WEEKLY;BYDAY=MO;UNTIL=20271231. One event row holds the rule and the first occurrence; instances are generated from it.

Exceptions

A single instance that differs from the rule: cancelled, moved, or retitled. An exception is keyed by the instance's original start time, so it can be matched to the generated instance it replaces.

Wall time and zones

Store an event's start as a local wall time plus a time zone name, such as 09:00 America/New_York, and compute UTC when needed. The zone name lets the system apply daylight-saving rules correctly for each date.

Sync tokens

A token that marks a position in a calendar's change history. A device sends its token and receives only the changes since then.

Interval overlap

An event overlaps a view range when it starts before the range ends and ends after the range starts. That one condition drives every view query.

Key idea. Rules plus exceptions for repetition, wall time plus zone for correctness, sync tokens for devices, and an overlap condition for views.

  1. Requirements

Before reading on. List the requirements. What is the property users would never forgive you for breaking?

1.1 Functional requirements

  • Create, update, and delete single and recurring events.
  • Edit one instance, this and following instances, or the whole series.
  • Invite attendees; attendees respond; the organizer sees responses.
  • View a day, week, month, or year quickly.
  • Sync changes to all devices within seconds; support offline edits.
  • Send reminders before events.

1.2 Non-functional requirements

  • Correct times across zones and daylight-saving changes.
  • View latency under 200 ms at p95.
  • Sync latency under a few seconds to online devices.
  • No lost edits, including offline edits.
  • High availability for reads.

1.3 The constraint versus the property

Correct events at the correct times are the property. A meeting shown an hour off is worse than a slow page. Read volume is the constraint. Phones and browsers read calendars far more often than anyone writes, so views must be cheap.

  1. Back-of-the-envelope estimation

  • Events: 500 M users × 5 per week = 2.5 billion new events a week, about 4,100 writes per second on average, perhaps 12,000 at peak.
  • Reads: users open the calendar several times a day, and every device syncs in the background. Assume 20 reads per write: about 80,000 reads per second on average.
  • Storage: 2.5 B × 1 KB = 2.5 TB a week of new events, about 130 TB a year before replication. Old events are rarely read; tier them.
  • Instances: if 30% of events recur weekly, and you materialize one year ahead, each recurring event adds 52 instance rows. That multiplies instance storage many times over the events table, which is the cost of materialization.
  • Invitations: with 3 attendees on average, each event change fans out to 4 calendars.
Key idea. Reads outnumber writes by an order of magnitude; recurring events and attendee fan-out multiply what each write touches.

  1. API design

Before reading on. When a user edits a recurring meeting, what three choices must the API support?

This instance only, this and all following instances, or the whole series. Each maps to a different data change.

GET  /v1/calendars/:id/events?from=2026-09-21T00:00&to=2026-09-28T00:00&tz=Europe/London
  -> instances in range (recurring ones expanded, exceptions applied)

POST   /v1/calendars/:id/events
  {title, start: {local: "2026-09-22T09:00", tz: "America/New_York"}, duration_min: 30,
   rrule?: "FREQ=WEEKLY;BYDAY=MO", attendees: [...]}
PATCH  /v1/events/:id?scope=instance|following|series&instance_start=...
  {fields..., if_version: 7}
DELETE /v1/events/:id?scope=...&instance_start=...
POST   /v1/events/:id/responses   {response: accepted|declined|tentative}

GET  /v1/calendars/:id/changes?sync_token=abc   -> {changes[], next_sync_token}
                                                 -> 410 if the token is too old

  1. Data model

calendars   (id, owner_id, name, default_tz)
events      (id, calendar_id, organizer_id, title, location, description,
             start_local, start_tz, duration_min,
             rrule NULL, series_until_utc NULL,
             version, updated_at, deleted_at)
exceptions  (event_id, original_start_local, status: cancelled|modified,
             new_start_local?, new_tz?, new_duration_min?, overrides_json)
attendees   (event_id, user_id, response, updated_at)
instances   (calendar_id, start_utc, end_utc, event_id, original_start_local)
            primary key (calendar_id, start_utc, event_id)     -- materialized window
changes     (calendar_id, seq, event_id, op, ts)                -- sync log
reminders   (user_id, event_id, minutes_before)

The instances table is derived. It can always be rebuilt from events and exceptions.

  1. High-level design

5.1 One row per event, times in UTC

Store each event with start and end in UTC, and query by range. Single events work. A weekly meeting is either one row, which the range query cannot see in later weeks, or infinite rows. And the 9:00 meeting moves to 8:00 or 10:00 after daylight saving changes.

5.2 Fix 1: wall time plus zone

Store start as local time plus a zone name, and convert to UTC per instance using that date's zone rules. The meeting stays at 9:00 New York time all year.

5.3 Fix 2: rules and exceptions

A recurring event is one row with a rule. Changes to one instance become exception rows keyed by the instance's original start.

5.4 Fix 3: a materialized instance window

A background job expands every recurring event into instance rows for a rolling window, such as one year ahead and some history. Views become one indexed range scan on (calendar_id, start_utc). Queries beyond the window, such as the year view for 2029, expand on read.

flowchart LR
  EV[(events + exceptions)]:::store --> EXP[Expander job]:::new --> INS[(instances: rolling 1-year window)]:::new
  Q[Week view]:::svc -->|range scan| INS
  Q2[View in 2029]:::svc -->|expand on read| EV
  classDef svc fill:#f4f1e8,stroke:#315e55,color:#171717;
  classDef store fill:#fdf3dc,stroke:#c4492d,color:#171717;
  classDef new fill:#ffffff,stroke:#c4492d,stroke-width:2px,stroke-dasharray:5 3,color:#171717;

5.5 Fix 4: a change log and sync tokens

Every change to a calendar appends to its change log with an increasing sequence number. Devices sync with a token; a push tells them to sync now.

5.6 The composed design

sequenceDiagram
  autonumber
  actor O as Organizer
  participant API as Calendar API
  participant DB as Events DB
  participant X as Expander
  participant CL as Change logs
  participant P as Push service
  participant D as Attendee devices
  O->>API: PATCH event (scope=series, if_version 7)
  API->>DB: update event, version 8
  API->>X: re-expand instances for organizer + attendee calendars
  X->>DB: rewrite instances in window
  API->>CL: append change to each affected calendar
  API->>P: notify devices of affected users
  P-->>D: "calendar changed"
  D->>API: GET changes?sync_token=...
  API-->>D: changes + next token
Key idea. Wall time with zones, rules with exceptions, a materialized window for fast views, and change logs with tokens for sync.

  1. Deep dives

6.1 Recurring events and their edits

Before reading on. A weekly Monday meeting runs for a year. The user moves only next Monday's meeting to Tuesday, then later changes the whole series to 10:00. What happens to next week's instance?

Moving one instance writes an exception: original start "Monday 09:00 on that date," new start "Tuesday 09:00." The series row does not change.

Changing the whole series to 10:00 updates the series row. The expander regenerates instances. For next week, it generates the Monday 10:00 instance from the rule, finds an exception keyed by the original start "Monday 09:00," and... the keys no longer match, because the rule now produces 10:00.

This is the classic trap. Two defensible answers:

  • Key exceptions by the original date, not time, for daily and weekly rules, and apply the series time change to exceptions that did not override the time. The moved instance stays on Tuesday, now at 10:00.
  • Ask the user. Calendar apps often warn that changing the series will reset modified instances, and drop the exceptions.

Name the trap and pick one. Interviewers are testing whether you see it.

"This and following" splits the series: set the old series' end before the chosen instance, and create a new series starting at it with the new values. Exceptions after the split move to the new series.

What separates answers: recurrence

WeakStores every instance forever, or none

Either materializes infinite rows or cannot show recurring events in range queries.

GoodRule plus exceptions

Stores the rule, keys exceptions by original start, and supports instance, following, and series edits.

StrongHandles the edit interactions

Explains what happens to exceptions when the series changes, splits series for "this and following," and keeps expansion in the event's own time zone.

6.2 Time zones and daylight saving

Store start_local and start_tz, never only UTC. Expand each instance by computing its UTC time from the local wall time and that date's zone rules. A 9:00 New York meeting is 13:00 UTC in summer and 14:00 UTC in winter; both are correct.

Duration is stored in minutes, not as an end time, so a meeting spanning a daylight-saving switch lasts the intended length.

Zone rules change: governments move daylight-saving dates. When the time zone database updates, re-expand future instances in affected zones. Events stored only in UTC cannot be corrected, which is one more reason for wall time plus zone.

All-day events have no time and no zone: store them as a date. A birthday on September 22 is September 22 everywhere.

6.3 Fast views

Before reading on. Write the query for a week view. Why does a long event cause trouble?

Overlap query on the materialized window:

SELECT * FROM instances
WHERE calendar_id = ANY(:calendars)
  AND start_utc < :range_end
  AND end_utc   > :range_start;

The index on (calendar_id, start_utc) narrows by start_utc < :range_end, but the end_utc > :range_start side is not bounded by the index. A two-week conference that started last week has start_utc well before the range and must still appear. Without care, the query scans back through every earlier event.

Fixes: cap the lookback, start_utc > :range_start - max_event_length, where events longer than, say, 7 days are kept in a small separate table that every view also reads. That bounds the scan.

Month and year views read more rows. Cache rendered month views per calendar and invalidate on change. The year view usually shows only busy days, so a per-day count table suffices.

6.4 Invitations

The organizer's event is the single source. Attendees get an attendees row, and each attendee's calendar shows the event through that row; their own instances are expanded into their calendar's instance table. An update by the organizer changes one event row and triggers re-expansion and change-log entries for every attendee calendar.

Copying the event into each attendee's calendar makes reads simple, but every update becomes a fan-out of writes that can drift apart. The reference model avoids drift; the fan-out still happens, but only for derived rows and change logs, which can be rebuilt.

Responses are per attendee, stored on the attendee row, and appear in the organizer's change log so their devices update the response count.

External attendees on other calendar systems receive standard invitation emails with an iCalendar attachment.

6.5 Sync across devices

Before reading on. A phone has been offline for three days. How does it catch up, and what if it was offline for six months?

Each calendar has an append-only change log with increasing sequence numbers. The device stores the last sequence it applied as its sync token. On reconnect, it asks for changes after its token. The server returns the changed events and the new token. The device applies them in order.

For immediate updates, the server sends a push notification or a message over an open connection saying "calendar X changed." The device then syncs. The push carries no data; the change log does, which keeps the protocol simple and reliable.

Change logs are trimmed after a retention window, such as 30 days. A device with an older token gets 410 Gone and does a full resync of a window around today, then continues incrementally.

6.6 Conflicts and offline edits

A phone edits an event offline while the organizer edits it on the web. Each edit carries the version it was based on. The server accepts the first; the second fails the if_version check. Options: field-level merge (take each changed field from whichever edit changed it, and flag true conflicts), or last-writer-wins per field. For calendars, field-level last-writer-wins is usually acceptable if you state it; the device shows a note when it overrode something.

6.7 Reminders

A reminder is due at instance start minus the chosen offset. Keep a reminder queue for the next day, built from the instance table and refreshed on changes. A scheduler service pops due reminders every second and sends notifications. Recomputing on every change is cheap because only the next day's reminders are materialized.

6.8 Expanding a rule, in code

Before reading on. Expand a weekly Monday 09:00 New York meeting into UTC instances for a date range, applying exceptions.
from datetime import datetime, timedelta, time
from zoneinfo import ZoneInfo

def expand_weekly(first_local_date, local_time, tz_name, weekday,
                  range_start_utc, range_end_utc, exceptions, until_local_date=None):
    tz = ZoneInfo(tz_name)
    utc = ZoneInfo("UTC")
    day = first_local_date
    while day.weekday() != weekday:
        day += timedelta(days=1)
    out = []
    while True:
        if until_local_date and day > until_local_date:
            break
        local = datetime.combine(day, local_time, tzinfo=tz)   # wall time in the event's zone
        start_utc = local.astimezone(utc)                       # correct offset for this date
        if start_utc >= range_end_utc:
            break
        ex = exceptions.get(day)                                # keyed by original local date
        if ex is None:
            out.append(start_utc)
        elif ex["status"] == "modified":
            out.append(ex["new_start_utc"])
        # cancelled: skip
        day += timedelta(weeks=1)
    return [s for s in out if s >= range_start_utc]

The key line is datetime.combine(day, local_time, tzinfo=tz): each instance is built from wall time on its own date, so the UTC offset follows daylight saving. Exceptions are looked up by the original local date. A real implementation also handles the rule forms of the iCalendar standard and wall times that do not exist or occur twice on transition days.

6.9 The daylight-saving edge cases

Two moments each year break naive code.

  • Spring forward. In New York, 02:00 to 02:59 does not exist on the transition day. A daily 02:30 event has no valid time. The standard practice is to move it forward by the gap, to 03:30.
  • Fall back. 01:00 to 01:59 happens twice. A daily 01:30 event is ambiguous. Pick the first occurrence consistently.

Name both, say which rule you apply, and put them in the test suite. Interviewers use them to check whether you store wall time or UTC.

What separates answers: time handling

WeakAdds seven days in UTC

Expands weekly events by adding fixed UTC intervals, drifting an hour at every daylight-saving change.

GoodExpands in the event's zone

Builds each instance from local wall time in the event's zone.

StrongHandles transitions

Also defines behavior for times that do not exist or repeat on transition days, and re-expands when zone rules change.

  1. Variants

7.1 Free-busy and scheduling assistant

Finding a time for several people reads each person's busy intervals for the range, from the instance table, and intersects free time. Privacy matters: return busy blocks, not titles.

7.2 Room booking

Rooms are calendars with a constraint: no double booking. Use a conditional insert or a lock on the room's time range, which needs stronger consistency than personal calendars.

7.3 Shared team calendars

Many users read one calendar. Cache its views aggressively and fan out change notifications to all subscribers' devices.

7.4 At ten times the users

At 5 billion users' worth of calendars, shard events and instances by calendar ID. Invitations then cross shards: an update writes the organizer's shard, then publishes a change event that each attendee's shard applies to its derived instances and change log. Accept that attendee views lag the organizer by a second or two, and say so.

  1. The transferable pattern

A calendar is a compact rule-based model with a materialized view for reads and a change log for sync. The same shape appears in subscription billing (rules generate invoices), scheduling systems, and any product where users edit rules but read instances: keep the rules as truth, derive the rows, and sync with sequence numbers.

Review: the 30-second answer

  • Wall time plus zone. Never UTC alone; expand per date.
  • Rules plus exceptions. Exceptions keyed by original start; split series for "this and following."
  • Materialized instance window. Overlap range scans with a bounded lookback; expand on read beyond it.
  • One organizer event, attendee references. Updates re-expand and log for each attendee.
  • Change logs with sync tokens. Push says "changed"; devices pull deltas; full resync if the token is too old.

Quiz

+Why store an event's start as local time plus a zone instead of UTC?

Recurring events keep their local time across daylight-saving changes, so their UTC time shifts. Local time plus a zone name lets the system compute the correct UTC time for each date, even when zone rules change.

+Why key an exception by the instance's original start time?

Generated instances are identified by where the rule places them. Keying the exception by that original position lets the expander match and replace the right instance, even after the instance was moved.

+What goes wrong when a long event overlaps the start of a view range?

It started before the range, so a query that only looks at start times in the range misses it. The query must include events that start earlier and end inside or after the range, with a bounded lookback to keep the scan small.

+How does a device catch up after being offline?

It sends its sync token, the last change sequence it applied, and receives only later changes. If the token is older than the retained log, it does a full resync of a window, then continues incrementally.

+Why does a push notification carry no event data?

The change log is the source of truth. The push only tells the device to sync, which keeps ordering, retries, and missed pushes simple: the next sync gets everything.

+Why build each instance from wall time on its own date?

The UTC offset of a zone changes with daylight saving. Building each instance from its local date and time gives the right offset for that date; adding fixed UTC intervals drifts by an hour.

+What should happen to a daily 02:30 event on the spring-forward day?

That time does not exist on the transition day. A common rule moves it forward by the gap, to 03:30. Whatever the rule, apply it consistently and test it.

Sources and further reading

NextURL Shortener