Low Level Design: Calendar and Scheduling System

Calendar and scheduling systems look deceptively simple on the surface but contain significant engineering depth. Recurring events, timezone transitions, multi-user scheduling, and real-time sync across devices and clients create a rich problem space. Industry implementations like Google Calendar and Apple Calendar have established protocols (CalDAV, iCalendar) that any serious calendar system should support. This post covers the full low level design from data model to sync protocol.

Event Data Model

The core event table stores: event_id (UUID), organizer_id, calendar_id, title, description, location, start_time (UTC timestamp), end_time (UTC timestamp), timezone (IANA tz string, e.g. America/New_York), recurrence_rule (RFC 5545 RRULE string), status (confirmed, tentative, cancelled), visibility (public, private, confidential), created_at, updated_at, sequence (integer, incremented on each update for sync).

Attendees are stored in a separate table: event_id, user_id, email (for external attendees), response (accepted, declined, tentative, needs-action), role (organizer, required, optional), responded_at. This allows querying all events for a user (WHERE user_id = ?) and all attendees for an event (WHERE event_id = ?) efficiently with the right indexes.

Calendars are first-class entities: calendar_id, owner_id, name, color, timezone (default timezone for events created in this calendar), is_primary. Users have multiple calendars (personal, work, shared team calendar). An event belongs to exactly one calendar.

Recurrence Rules

Recurring events are stored as a single event record with an RRULE string rather than materializing all occurrences. This keeps storage linear rather than exploding for events like "daily standup, weekdays only, indefinitely." Example RRULE values:

FREQ=WEEKLY;BYDAY=MO,WE,FR
FREQ=MONTHLY;BYDAY=1MO  (first Monday of each month)
FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1  (New Year's Day)
FREQ=WEEKLY;BYDAY=MO,WE;UNTIL=20261231T235959Z

Occurrence expansion happens at query time: given a date range, the recurrence engine expands the RRULE and returns the occurrence timestamps that fall within the range. Libraries like rrule.js (client) or python-dateutil (server) implement the RFC 5545 expansion algorithm. Caching expanded occurrences up to N months out avoids repeated computation.

Modifying a single occurrence of a recurring event creates an exception record (recurrence_exception table): parent_event_id, original_start_time (identifies which occurrence), exception_event_id (the modified event record). The exception event is a full event record with its own title, time, attendees, etc. When expanding recurrences, the engine skips original occurrences that have exceptions and substitutes the exception events. Deleting a single occurrence creates a EXDATE entry instead of a full exception record.

Timezone Handling

Timezone handling is where calendar systems most often break. The key principle: store start_time and end_time in UTC, but also store the original timezone string. Never store only a UTC timestamp for recurring events—you lose the original intent.

Consider a daily 9:00 AM meeting in America/New_York. Stored as UTC, the first occurrence in January is 14:00 UTC. After the US clocks spring forward in March, the recurrence should still be 9:00 AM Eastern, which is now 13:00 UTC. If you only stored 14:00 UTC and repeated it, the meeting would show at 10:00 AM after DST transition—wrong. The correct approach is to anchor the recurrence rule to the named timezone and expand each occurrence by interpreting the local time in that timezone, then converting to UTC for storage and comparison.

Display timezone is the viewer's local timezone, which may differ from the event's timezone. The UI converts UTC start/end to the viewer's timezone for display. iCalendar export includes a VTIMEZONE component describing the timezone rules, ensuring the event displays correctly in any compliant client even without internet access to look up tz data.

Conflict Detection

Conflict detection is a range overlap query. Two events conflict if event_a.start < event_b.end AND event_a.end > event_b.start. For a given attendee and proposed time slot, the free/busy query fetches all events where the attendee is a participant and the event overlaps the proposed time.

The scheduling assistant extends this to multi-attendee scenarios. Given a list of required attendees, a duration, and a search window, the algorithm:

  1. Fetch free/busy data for all attendees within the search window.
  2. Merge busy intervals per attendee into a unified busy set.
  3. Find gaps in the union of busy intervals that are at least duration long.
  4. Apply working-hours constraints (only suggest 9 AM–5 PM slots).
  5. Return the first N available slots.

Free/busy queries are performance-sensitive. The attendees table index on (user_id, event_start_time, event_end_time) is critical. For large organizations, precomputing and caching free/busy windows in Redis (updated on every event write) reduces query latency from database scans to cache lookups.

Invite and RSVP Workflow

When an organizer creates an event with attendees, the system sends invite notifications. Each attendee receives an email with Accept, Decline, and Tentative links. These links contain a signed token encoding event_id + attendee_id + action to prevent CSRF and allow one-click response without requiring login.

On RSVP: update attendees.response and attendees.responded_at. Notify the organizer of the response (aggregate: "5 accepted, 2 declined, 3 pending"). If the attendee declines, optionally suggest alternative times using the scheduling assistant.

Reminders are stored as event_reminders: event_id, user_id, method (email, push, SMS), minutes_before. A scheduler job runs every minute, queries for reminders due in the next minute, enqueues notification tasks, and marks reminders as sent. For recurring events, the reminder is relative to each occurrence's start time, not the series start.

CalDAV Protocol

CalDAV (RFC 4791) extends WebDAV to enable calendar data sync. It defines a collection hierarchy: principalcalendar homecalendarcalendar object resource (individual VEVENT files). Clients sync via HTTP requests against these resources.

The sync protocol centers on sync-token: an opaque string representing the current state of a calendar collection. On first sync, the client does a PROPFIND to get all event ETags and REPORT to fetch their content. On subsequent syncs, the client sends a sync-collection REPORT with the last known sync-token. The server returns only the resources changed since that token—added, modified, deleted. This makes incremental sync efficient even for large calendars.

Each event is serialized as an iCalendar (.ics) file containing VCALENDARVEVENT components. The VEVENT includes UID, DTSTART, DTEND, RRULE, EXDATE, ATTENDEE lines with PARTSTAT (participation status), and ORGANIZER. Supporting CalDAV means any compliant client (Apple Calendar, Thunderbird, BusyCal) can sync with your server without a custom app.

Notification System

Calendar notifications span multiple channels and timing patterns. The notification service reads from an event stream (Kafka topic receiving all event create/update/delete operations) and maintains a scheduled notification queue.

Reminder notifications are scheduled at event_start - reminder_offset. The scheduler uses a sorted set in Redis (ZADD reminders {timestamp} {reminder_id}) as a priority queue. A worker polls with ZRANGEBYSCORE reminders 0 {now} every 30 seconds, dequeues due reminders, and dispatches them. For recurring events, the next occurrence's reminder is scheduled when the current occurrence's reminder fires.

Real-time notifications (meeting starting in 5 minutes) are pushed via WebSocket to online clients. The presence service tracks which users are connected; the notification service checks presence before deciding between WebSocket push and email/push fallback. Daily agenda emails are sent at a user-configured time (default 7 AM in user's timezone), summarizing the day's events.

Sharing and Permissions

Calendar sharing uses a role-based model: owner (full control, can delete calendar), editor (create, edit, delete events), viewer (read all event details), freebusy (see only free/busy blocks, no titles or details). Permissions are stored in a calendar_shares table: calendar_id, user_id (or email for pending shares), role, shared_at.

Event-level visibility overrides calendar defaults. An event marked private on a shared calendar shows only as "Busy" to viewers without owner access. Confidential events are visible by title to editors but attendee details are hidden.

Public calendar URLs allow subscription without authentication. The server generates a secret token URL (/cal/{calendar_id}/{secret_token}.ics) that serves the calendar in iCalendar format. External clients poll this URL periodically (typically every 15–60 minutes) to get updates. Revoking the secret token invalidates all external subscriptions, issuing a new token regenerates access.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “How do you parse RRULE recurrence rules and generate future occurrences efficiently?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Parse the RRULE string (RFC 5545) into a structured object capturing FREQ, INTERVAL, BYDAY, UNTIL/COUNT, and EXDATE fields. Use an iterator pattern to lazily generate occurrences: start from DTSTART, advance by the interval, apply BYxxx filters, and skip EXDATEs. For bounded queries (e.g., “events in next 30 days”), stop generation early rather than materializing all instances. Store only the rule and exceptions; expand on read or pre-materialize into a rolling window for search indexing.” } }, { “@type”: “Question”, “name”: “How do you handle timezone changes and DST transitions for recurring events?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Store event times as wall-clock time plus IANA timezone identifier (e.g., America/New_York), never as a fixed UTC offset. When expanding recurrences, resolve each occurrence independently using the tz database so that a weekly Monday 9am event always fires at 9am local time regardless of DST shifts. For all-day events, store as a date without time. Convert to UTC only at query time for cross-user comparison and index on both the UTC instant and the local date for efficient range queries.” } }, { “@type”: “Question”, “name”: “How do you design conflict detection and free/busy queries in a calendar system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Maintain an interval index per user (e.g., a sorted set of [start_utc, end_utc, event_id] tuples). For conflict detection, query for any interval overlapping [proposed_start, proposed_end] using the condition existing_start proposed_start. For free/busy aggregation across attendees, execute parallel interval lookups, merge the sorted results, and return gaps. Partition the index by user_id and shard by time range to keep lookup cost O(log n + k) where k is the number of overlapping events.” } }, { “@type”: “Question”, “name”: “How does CalDAV sync work and how do you implement token-based incremental sync?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “CalDAV (RFC 4791) uses WebDAV REPORT requests to sync collections. Implement a sync-token (opaque server-assigned version, e.g., a logical clock or content hash of the collection) returned with every response. Clients store the token and send it with the next sync-collection REPORT; the server returns only objects changed or deleted since that token. Store a changelog table (object_id, change_type, sync_token) and respond with a delta. This reduces bandwidth from O(collection_size) to O(changes_since_last_sync).” } }, { “@type”: “Question”, “name”: “How do you implement an invite and RSVP workflow with calendar integration?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “When an organizer creates an event with attendees, generate an iCalendar (ICS) object with METHOD:REQUEST and email it to each attendee. Each attendee’s calendar client responds with METHOD:REPLY carrying PARTSTAT (ACCEPTED/DECLINED/TENTATIVE). The organizer’s server processes replies, updates attendee status in the event record, and sends a METHOD:COUNTER for proposed time changes. Surface RSVP state in the UI and expose it via API. Use idempotency keys on reply processing to handle duplicate delivery from retried emails.” } } ] }

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

See also: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale

See also: Atlassian Interview Guide

Scroll to Top