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:
- Fetch free/busy data for all attendees within the search window.
- Merge busy intervals per attendee into a unified busy set.
- Find gaps in the union of busy intervals that are at least
durationlong. - Apply working-hours constraints (only suggest 9 AM–5 PM slots).
- 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: principal → calendar home → calendar → calendar 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 VCALENDAR → VEVENT 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.
See also: Atlassian Interview Guide