cswork / libraryOpen practice workspace

Articles

Calendar System Design Interview Guide: Recurrence, Time Zones, Conflicts, and Notifications

Learn calendar system design for interviews: model recurrence, handle time zones and DST, prevent conflicts, sync offline clients, and deliver reminders.

A strong calendar system design answer starts with one rule: preserve what the user meant before optimizing how the system serves it. Store a recurring event as a series plus exceptions, keep its civil time and IANA time-zone identifier, expand only a bounded window into occurrences, and make each reminder and mutation idempotent. Conflict handling then becomes a precise interval problem with a policy boundary: warn for ordinary calendars, but serialize writes when a shared resource must never be double-booked.

This guide is for software engineers preparing for a system design round. The goal is not to reproduce one vendor's internals; it is to present a coherent design that stays correct through recurrence edits, daylight-saving transitions, concurrent booking, offline clients, and notification retries.

Calendar system design: the quick answer

A calendar system stores authoritative event intent, derives bounded occurrences for reads and free/busy checks, applies access and conflict policy at write time, publishes every committed change, and schedules reminders from the committed event version.

Use an Anchor → Expand → Reserve → Deliver framework:

StageAuthoritative decisionCommon failure to prevent
AnchorStore the series rule, local civil time, time zone, duration, attendees, ACL, and versionConverting a weekly 9:00 meeting into a fixed UTC pattern
ExpandGenerate occurrences only for the requested or materialized horizonExpanding an unbounded recurrence forever
ReserveQuery half-open busy intervals and apply warn, reject, or override policyA check-then-write race that double-books a room
DeliverEmit change records, sync deltas, invitations, and reminder jobs idempotentlyDuplicate or stale notifications after retries and edits

This separation gives the interview a clean spine. The event store owns user intent; occurrence and free/busy indexes are rebuildable projections; queues isolate slow external delivery from the write path.

Clarify scope and define the API contract

Start by asking whether the product supports personal calendars, shared calendars, required attendees, rooms, all-day events, recurring events, invitations, offline devices, and multiple notification channels. Clarify whether conflicts are advisory or forbidden, how far ahead availability searches must look, and whether cross-organization federation is in scope.

Define invariants before drawing boxes:

  • Every committed mutation has a stable event_id, monotonically increasing version, actor, and idempotency key.
  • Timed events use half-open intervals [start, end), so a meeting ending at 10:00 does not conflict with one starting at 10:00.
  • A recurring occurrence has stable identity even if it is moved.
  • Viewers without detail permission see an opaque busy block, not private title or attendees.
  • Notifications are derived from a committed version and become invalid when that version changes.

A compact API might expose POST /events, GET /events?time_min=&time_max=&page_token=, PATCH /events/{id} with If-Match, DELETE /events/{id}, GET /freebusy, and POST /events/{id}/responses. An Idempotency-Key prevents a mobile retry from creating two events. An entity tag plus If-Match prevents one editor from silently overwriting another; RFC 9110 defines this conditional-request behavior.

Model recurrence as a series plus exceptions

The canonical record should contain event_id, organizer calendar, start and end intent, time zone, recurrence set, attendee and visibility data, status, and version. Store attendee state separately because each invitee can accept, decline, hide details, or configure reminders independently.

RFC 5545 defines recurrence using RRULE, explicit inclusions with RDATE, exclusions with EXDATE, and RECURRENCE-ID for a particular instance. The recurrence identifier is based on the original start, so it remains stable when that occurrence moves. A practical schema therefore uses:

  • one series master containing DTSTART, duration, time zone, and recurrence set;
  • one exception row keyed by (series_id, original_start) for a moved, edited, or canceled occurrence;
  • an optional series segment when “this and following” changes the future rule.

Do not store every future occurrence of an endless weekly meeting. Expand on demand for the requested view and materialize a rolling horizon—perhaps the product's usual availability window—as a cache. Rebuild that cache whenever the master or an exception changes. Google Calendar's recurring-event model similarly distinguishes a parent recurring event, instances, and exceptions, and warns that editing instances one by one creates excessive exceptions.

Keep time zones and daylight saving correct

Time is both a civil-time promise and an instant. “Every Monday at 9:00 in America/Los_Angeles” should remain at 9:00 after a daylight-saving change, while a flight checkpoint or machine job might intentionally stay at a fixed UTC instant. Ask which semantic the product needs.

For a wall-clock event, store the local date-time, IANA zone ID, and duration; derive UTC instants using the time-zone database when expanding occurrences. Do not store only a numeric offset such as -08:00: it does not encode future daylight-saving rules. The IANA Time Zone Database is periodically updated as governments change boundaries and clock rules, so record the tzdb version used for expansion and plan a controlled re-expansion when rules change.

Define behavior for nonexistent spring-forward times and ambiguous fall-back times. The API can reject and ask the client to choose, or apply a documented policy and return the resolved offset. All-day events should remain date ranges in the event's calendar zone, not midnight UTC timestamps. For cross-zone views, compute overlap in UTC instants but format each participant's display from their selected zone.

Detect conflicts and make hard bookings atomic

For two half-open intervals, overlap exists when max(start_a, start_b) < min(end_a, end_b). A sorted (calendar_id, start_instant) index can inspect nearby candidates for light personal calendars. At larger read volume, maintain a denormalized free/busy projection partitioned by calendar and time bucket, but keep it rebuildable from canonical events.

The critical distinction is policy. A personal calendar can return warnings and alternatives, then accept an explicit override. A conference room, clinician, or appointment slot may require a hard invariant: no two active reservations overlap. In that path, the conflict check and insert must share one serializable transaction, per-resource lock, or database constraint. PostgreSQL documents range types and exclusion constraints that can reject overlapping reservations at commit time.

Recurring conflicts need a bounded decision. Expand the proposed series and existing busy intervals for the agreed horizon, return conflicts per occurrence, and state what happens beyond it. A background checker can extend the horizon and alert the organizer if a later conflict appears. For multi-attendee suggestions, intersect normalized free intervals rather than exposing private event details.

Scale the architecture without losing the source of truth

Place an API gateway and authorization layer before an event service. The event service writes canonical events, attendees, ACLs, and an outbox record atomically. Change-data capture or an outbox publisher fans the committed version to recurrence expansion, free/busy indexing, invitations, reminder scheduling, search, and analytics.

Partition canonical data by organizer calendar or tenant so most reads and writes remain local. Partition occurrence projections by (calendar_id, time_bucket) to support range queries. Popular room calendars and large public calendars can become hot keys; isolate them, cache public reads, and apply admission control to hard-booking writes. Avoid a global transaction across every attendee calendar: the organizer's event can commit once, while attendee copies and responses converge through idempotent events.

Treat projections as disposable. Store a source version with every occurrence and free/busy row, measure lag, compare sampled projection checksums with canonical expansion, and provide replay/rebuild tooling. In a regional failure, route reads to a replica only if its staleness is visible; keep hard resource booking on a single authoritative write region or consensus boundary.

Make reminders and offline sync version-aware

When an event commits, create reminder intents keyed by (event_id, occurrence_id, event_version, recipient, channel, offset). A scheduler scans due buckets and sends jobs to per-channel queues. Workers re-read cancellation, version, recipient preference, quiet hours, and device validity before delivery. This prevents a reminder for the old 10:00 meeting from firing after it moves to 11:00.

Use at-least-once processing with unique delivery keys, bounded retries, exponential backoff with jitter, expiry, and a dead-letter path. Providers can time out after accepting a request, so exactly-once user delivery is usually not provable; deduplication reduces duplicates. The Web Push protocol defines TTL, urgency, and topic controls, while FCM's scale guidance recommends throttling and jittered backoff.

Offline clients need ordered deltas, tombstones, and a resync path. Return a page token while a snapshot is paginating, then a durable sync token for subsequent changes. If the token expires, require a fresh snapshot instead of guessing from timestamps. Google Calendar's incremental-sync guide documents this full-sync, incremental-sync, and invalid-token recovery pattern.

Run the interview in 45 minutes

  1. Minutes 0–5: clarify event types, recurrence, conflict policy, attendees, privacy, offline use, notification channels, and scale shape.
  2. Minutes 5–10: state invariants, API semantics, half-open intervals, idempotency, and optimistic concurrency.
  3. Minutes 10–18: draw the canonical series, exception, attendee, ACL, outbox, occurrence, and reminder records.
  4. Minutes 18–27: walk one recurring create through time-zone conversion, bounded expansion, conflict query, transaction, and response.
  5. Minutes 27–34: deep-dive on a hard room-booking race and compare locking, serializable isolation, and an exclusion constraint.
  6. Minutes 34–40: cover reminder retries, stale-version suppression, invitations, incremental sync, and offline conflicts.
  7. Minutes 40–45: address hot calendars, projection drift, regional failure, metrics, and the largest remaining trade-off.

Good observability follows the lifecycle: write latency and conflicts, outbox lag, expansion lag, free/busy correctness samples, hard-booking aborts, sync-token resets, reminder lateness, provider error classes, dedup hits, and stale-version drops. Avoid invented capacity numbers; state assumptions and show how measurements change partitioning or caching choices.

Frequently asked questions

Should a calendar store UTC or local time?

Store both the event's intended local civil time and IANA zone when wall-clock behavior matters, then derive UTC instants for indexing and overlap checks. A fixed-instant event can be UTC-first. Storing only UTC loses the “every Monday at 9:00 local” promise; storing only local time makes cross-zone ordering ambiguous.

How should recurring events be stored?

Store one series master with its recurrence set and stable start semantics, plus sparse exceptions keyed by the original occurrence start. Expand only the requested or materialized horizon. This avoids infinite rows while supporting edit-one, cancel-one, edit-series, and “this and following” behavior without losing occurrence identity.

How do you prevent two users from booking the same room?

Put the final overlap check and reservation write inside one correctness boundary for that room: serializable transaction, per-resource lock, or database exclusion constraint. A cache-only check is insufficient because two callers can both observe availability before either write becomes visible. Return a conflict or retry outcome deterministically.

How do calendar reminders avoid duplicates and stale sends?

Key each reminder by event, occurrence, version, recipient, channel, and offset. Workers must verify the current event version and cancellation state before calling a provider, then record the delivery attempt idempotently. Retries can still produce ambiguous provider outcomes, so use deduplication, expiry, and honest at-least-once semantics.

How does offline calendar sync recover from missed changes?

Clients perform an initial paginated snapshot, persist the server-issued sync token, and request deltas that include updates and tombstones. Each response advances the token only after all pages are applied. If the server invalidates an old token, the client clears its projection and performs a fresh snapshot rather than silently skipping history.

Final takeaway

Calendar system design is not one interval index wrapped in an API. A strong answer preserves civil-time intent, models recurrence as a master plus exceptions, expands a bounded horizon, distinguishes advisory conflicts from atomic resource reservations, and makes every downstream projection and reminder version-aware. Present the Anchor → Expand → Reserve → Deliver path, then pressure-test it with DST, concurrent room booking, offline edits, and duplicate delivery.

Sources and Further Reading

Research note: This guide was checked on September 1, 2026. Standards, APIs, time-zone rules, and provider behavior can change; verify current documentation before implementation.