Calendar Service Low-Level Design: Event Model, Recurring Events, and Conflict Detection

Event Schema

The event is the core entity. It supports single and recurring events, multiple attendees, and rich metadata:

events {
  event_id            UUID PK
  calendar_id         UUID FK
  title               VARCHAR(255)
  description         TEXT
  start_time          TIMESTAMP WITH TIME ZONE  -- stored in UTC
  end_time            TIMESTAMP WITH TIME ZONE
  timezone            VARCHAR(64)   -- e.g. "America/New_York"
  location            VARCHAR(500)
  recurrence_rule     TEXT nullable  -- RRULE string per RFC 5545
  recurrence_exceptions DATE[]       -- dates where normal occurrence is skipped
  organizer_user_id   UUID FK
  visibility          ENUM(public, private)
  status              ENUM(CONFIRMED, TENTATIVE, CANCELLED)
  created_at          TIMESTAMP
  updated_at          TIMESTAMP
}

event_attendees {
  event_id  UUID FK
  user_id   UUID FK nullable
  email     VARCHAR(255)
  status    ENUM(NEEDS_ACTION, ACCEPTED, DECLINED, TENTATIVE)
}

Timezone Storage

All times are stored in UTC in the database. The timezone field records the event's originating timezone. This is critical for recurring events: RRULE evaluation must happen in the event's timezone to correctly handle Daylight Saving Time transitions. A weekly event on Monday at 9am ET stays at 9am ET through DST changes — UTC offset varies, but local time is stable.

Recurring Events

Recurring events store a single master record with an RRULE string rather than pre-generating all instances. Example:

RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12
RRULE:FREQ=MONTHLY;BYDAY=1MO;UNTIL=20241231T000000Z

Instances are generated on-demand when the calendar view requests a date range. The server evaluates the RRULE in the event's timezone, filters to the requested window, and returns the list of occurrence timestamps. Libraries like python-dateutil or rrule.js handle RRULE parsing.

Recurring Instance Modification

Two types of modifications to a recurring series:

  • Single occurrence edit: add the occurrence date to recurrence_exceptions on the master event; create a standalone modified_instance record with the exception date and changed fields. The master event's RRULE skips this date; the modified instance is served instead.
  • This and all future: set UNTIL on the original RRULE to end before the split date; create a new master event starting at the split date with the updated fields and a new RRULE.

Conflict Detection

To detect scheduling conflicts for a user, query their events for time overlap with the proposed slot:

SELECT e.event_id, e.title
FROM events e
JOIN event_attendees a ON e.event_id = a.event_id
WHERE a.user_id = :user_id
  AND e.status != 'CANCELLED'
  AND e.start_time < :new_end_time
  AND e.end_time   > :new_start_time

This interval overlap condition (start < other_end AND end > other_start) catches all overlapping cases. For recurring events, generated instances are checked against the proposed slot during RRULE evaluation.

Multi-Attendee Scheduling

Finding a free slot for multiple attendees works by computing the intersection of all their free windows:

  1. Query all events for each attendee within the search window
  2. Build a busy-time list per attendee
  3. Compute free intervals: subtract busy times from the full search window
  4. Intersect free intervals across all attendees
  5. Filter resulting slots by minimum duration and business hours
  6. Return ranked slot suggestions

iCal Import (RFC 5545)

Uploaded .ics files are parsed component by component. VEVENT components map to event records. VTIMEZONE components define timezone rules used to convert local times to UTC. Recurrence rules (RRULE, EXDATE) are stored as-is. ATTENDEE properties map to event_attendees rows. Duplicate detection uses the UID field from the iCal spec — if an event with that UID already exists, the import updates rather than duplicates it.

iCal Export and Calendar Subscription

Events are exported as RFC 5545 compliant .ics files. For calendar subscriptions, a user's calendar is served at a stable URL:

GET /calendars/{calendar_id}/feed.ics?token={private_token}

External calendar apps (Google Calendar, Apple Calendar, Outlook) poll this URL on a schedule to sync events. The feed includes all events in the calendar serialized as VEVENT blocks, with proper RRULE fields for recurring events.

Attendee Invitations

When an event is created with attendees, invitation emails are sent with Accept / Decline / Tentative links. Each link carries a signed token encoding {event_id, attendee_email, response}. Clicking the link updates the attendee's status and redirects to a confirmation page. The organizer sees real-time RSVP status updates as attendees respond.

Calendar Sharing

Calendars can be shared between users with two permission levels:

  • View-only: shared user sees events but cannot create or edit
  • Edit: shared user can create, modify, and delete events on the calendar
calendar_shares { calendar_id, shared_with_user_id, permission ENUM(view, edit) }

Private events (visibility = private) are shown as “Busy” blocks to view-only users, hiding title and description.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you design the core event data model for a calendar service that supports all-day events, multi-day spans, and time zones?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store events with: id, calendar_id, title, description, start_utc (TIMESTAMPTZ), end_utc (TIMESTAMPTZ), start_timezone (IANA tz name, e.g., America/New_York), is_all_day (boolean), rrule (text, RFC 5545 RRULE string), recurrence_exception_dates (array of dates). Always persist in UTC and convert to the user's local timezone at display time to avoid ambiguity during DST transitions. For all-day events, store as DATE type (no time component) to avoid timezone conversion issues. Index on (calendar_id, start_utc, end_utc) to support range queries for week/month views efficiently.”
}
},
{
“@type”: “Question”,
“name”: “How do you expand recurring events defined by RRULE for a given date range without pre-materializing all occurrences?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store the RRULE string on the master event record (e.g., FREQ=WEEKLY;BYDAY=MO,WE;UNTIL=20261231T000000Z). At query time, use an RFC 5545-compliant RRULE expansion library (e.g., rrule.js, python-dateutil, or Google's rfc5545 library) to generate only the occurrences that intersect the requested date window. This avoids storing potentially thousands of occurrence rows. Maintain an event_exceptions table (master_event_id, original_occurrence_date, modified_event_id or is_deleted) for occurrences that have been individually edited or cancelled. When expanding, filter out exception dates and substitute modified occurrences.”
}
},
{
“@type”: “Question”,
“name”: “How do you detect scheduling conflicts between a new event and existing events on a user's calendar?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Two events conflict if their time ranges overlap: event A conflicts with event B when A.start_utc B.start_utc. In SQL: SELECT id FROM events WHERE calendar_id = :cid AND start_utc :new_start AND id != :new_id. For recurring events, expand the RRULE for the query window first, then test each occurrence against the same overlap condition. Index on (calendar_id, start_utc) and use an exclusion constraint (PostgreSQL's EXCLUDE USING gist) on a tstzrange column for database-level enforcement. Return conflicts to the caller but allow them to override (soft conflict) or block saving (hard conflict) based on business rules.”
}
},
{
“@type”: “Question”,
“name”: “How would you implement a meeting invite system with attendee RSVP tracking and calendar updates propagated to all invitees?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Model invites with an event_attendees table: (event_id, user_id, email, rsvp_status [NEEDS_ACTION, ACCEPTED, DECLINED, TENTATIVE], is_organizer). The organizer creates the event and the system sends iCalendar (RFC 5545) METHOD:REQUEST emails to invitees. Each invitee's response triggers an iCal METHOD:REPLY email back, updating their rsvp_status in the table. When the organizer updates the event (time change, cancellation), send METHOD:UPDATE or METHOD:CANCEL messages to all attendees; copy the updated event into each attendee's calendar via their calendar_id. Use a message queue to fan out notifications asynchronously so organizer saves remain fast regardless of attendee count.”
}
}
]
}

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture

See also: Atlassian Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top