Google Calendar
Design a scalable calendar service that handles event creation, recurring schedules, free/busy queries, and real-time UI updates for hundreds of millions of users.
TL;DR
Store one base row for each event or recurring series, keep the RRULE and timezone with the series, and represent per-occurrence edits or cancellations in a RecurrenceException table. Use overlap-aware indexes for one-off events and an indexed forward-occurrence marker for bounded recurrence queries. Strongly commit event and attendee writes on the primary, serve read-heavy ranges from replicas or a per-user cache, and publish lightweight change events after commit for WebSocket clients. Derive free/busy bitmaps from canonical events; for busy-bitmaps, union attendee busy minutes with BITOP OR and take the complement to find minutes free for everyone.
The key invariants are that event times are interpreted in the event timezone before UTC storage/serving, a recurrence exception is keyed by the base series and original occurrence, attendee authorization is enforced, and derived caches, notifications, and replicas can lag without becoming the source of truth.
Scope and assumptions
- The service covers authenticated event CRUD, RRULE-based recurrence, attendee responses, time-range queries, free/busy lookup, and real-time change notifications. Video-conference links, AI scheduling, calendar delegation/sharing, and broad analytics remain below the line.
- The scale figures are illustrative planning assumptions: 500M users, roughly 25B logical events/series, 10M peak range queries per second, 200ms p99 reads, 500ms p99 writes, and 1-2 seconds of replica lag.
- Store instants in UTC plus an IANA timezone identifier for recurrence expansion. Range APIs use a half-open interval
[from, to)unless a product contract specifies otherwise; this avoids double-counting adjacent events. - The
next_occurrence_dateoptimization targets calendar windows near the current horizon. Arbitrary historical or far-future recurrence queries need series bounds, RRULE-aware skipping, or a materialized occurrence index; a single next-future pointer is not sufficient for every possible date range. - Free/busy Redis bitmaps are derived and rebuildable. This design encodes
1 = busy, so the union of busy bits is an OR; an availability bitmap could instead use AND directly.
What is a calendar service?
A calendar service stores events and schedules for users and helps them coordinate with others. The apparent simplicity hides two hard problems: (1) modeling recurring events without storing one database row per occurrence (a weekly meeting for two years is 104 occurrences but only one logical rule), and (2) efficiently querying "show me this user's events between Monday and Sunday" when recurring events must be expanded dynamically at query time. These two problems, recurrence modeling and range query efficiency, drive every significant architectural decision in this design.
The useful framing is: βThe interesting part is not CRUD on events. It is what happens when FREQ=WEEKLY meets a date-range query months away.β That directs attention to recurrence representation, exceptions, time zones, and range-query cost.
Functional Requirements
Core Requirements
- Users can create, update, and delete calendar events (title, start time, end time, location, description).
- Events can recur on a schedule (daily, weekly, monthly, custom RRULE per the iCalendar spec).
- Users can invite others to events; invitees can accept or decline.
- Users can query their calendar for a time range (for example, all events in January).
Below the Line (out of scope)
- Video conferencing integration (Zoom/Meet links)
- Smart scheduling AI (find free time for all attendees)
- Calendar sharing and delegation (view another user's full calendar)
The hardest part in scope: Recurring events. Storing the rule once vs expanding all instances is the central schema design tension, and it cascades into every downstream decision: range queries, "update all future occurrences" operations, per-occurrence exceptions, and free/busy computation.
Video conferencing integration is below the line because it does not change the event storage or query path. To add it, store a conference_url field on the event and call the provider API (Zoom or Meet) on event creation to generate the link. The URL is metadata; it does not affect recurrence logic.
Smart scheduling AI is below the line because it is a separate read service that sits above the free/busy layer. To add it, query all attendees' free/busy bitmaps, compute the intersection, and return suggested time slots. The underlying free/busy data we build in this design is already the required input.
Calendar sharing and delegation is below the line because it requires a permission model (owner, viewer, editor) and row-level access checks on every calendar query. To add it, maintain a calendar_permissions table mapping (owner_id, grantee_id, permission_level) and gate every query on that table. The event schema does not need to change.
Non-Functional Requirements
Core Requirements
- Scale assumption: 500M users, each averaging 50 events per month, gives roughly 25B events total. 100M DAU querying their calendars at peak produces around 10M range queries per second.
- Latency targets: Calendar range queries return in under 200ms p99. Event creation completes in under 500ms p99.
- Availability target: 99.99% uptime. A user who cannot see their own calendar is a critical failure.
- Consistency target: Eventual consistency is acceptable for read replicas (a newly created event appearing within 1-2 seconds is fine). Writes are strongly consistent on the primary.
Below the Line
- Sub-millisecond query latency (requires hot in-memory serving for all 25B events, not practical at this scale)
- Multi-region strong consistency (cross-region replication lag is acceptable under the eventual consistency model)
Read/write ratio: Calendar is read-heavy at roughly 10 reads per write. Users browse their calendar far more often than they create events. This ratio directly determines the caching strategy: pre-computing and caching the "next 30 days" snapshot per user is viable because writes are infrequent enough to make cache invalidation cheap.
The 10:1 ratio means the read path deserves most of the optimization effort. Adding read replicas, caching pre-expanded event windows in Redis, and putting an index on (user_id, start_time) all pay off in this workload. The write path does not need a queue or stream processor solely for throughput; the illustrative 1M writes per second still needs to be validated against the chosen schema and primary-database capacity.
Call out the ratio early. Unlike a top-K or analytics pipeline, the stated workload does not require a queue merely to buffer ordinary event writes; the read path is the first optimization focus. A queue can still be useful for notifications, bitmap rebuilds, or other asynchronous side effects.
30-second answer / outline
βI would use a relational primary for event, attendee, and recurrence metadata. One recurring series stores an RRULE and timezone; per-occurrence moves and cancellations live in an exception table. Range queries use a half-open overlap predicate and user/time indexes, while a forward-occurrence marker narrows the recurring candidates for near-term windows. Read replicas and a short-lived per-user cache absorb reads. Event and attendee writes commit first, then an outbox/pub-sub path drives notifications and WebSocket invalidations. Free/busy is a rebuildable busy-minute bitmap: OR attendee bitmaps to get the union of busy time, then complement it for common free slots.β
5-minute explanation
- Start with the model: one-off events and recurring series share an event representation; RRULE and timezone define the series, and exceptions identify one original occurrence without rewriting the base rule.
- Walk the range query: use
start_time < to AND end_time > fromfor one-offs. For recurring series in the supported horizon, usenext_occurrence_dateto select candidates, expand RRULEs in the application layer, apply exceptions, and merge-sort results. - Explain writes and collaboration: event and attendee rows commit transactionally on the primary. After commit, asynchronous invitation delivery and user-scoped change events update WebSocket clients; replicas and caches are derived and can lag.
- Explain free/busy: maintain minute-granularity busy bitmaps as a derived cache, rebuild missing days from canonical events, and use Redis bit operations for multi-attendee union/intersection semantics.
- Close on time and reliability: expand in the event timezone, store UTC instants, version recurrence jobs, invalidate affected windows, and monitor replica/cache lag, recurrence expansion cost, notification delivery, and bitmap rebuild failures.
45-minute interview approach
This is an interview plan for the design question, not a claim that the article should take 45 minutes to read.
- 0-5 minutes β Clarify scope: confirm one-off versus recurring events, update modes, time zones, attendee visibility, historical/future query horizons, free/busy granularity, and notification expectations.
- 5-10 minutes β Requirements and capacity: state the illustrative user/event/query assumptions, read/write ratio, latency, availability, replica-consistency, and recurrence-window constraints.
- 10-17 minutes β APIs and data model: sketch event CRUD, range reads, attendee responses, change subscriptions, and the Event/Attendee/RecurrenceException relationships.
- 17-25 minutes β Baseline and recurrence: draw a relational one-off design, then evolve to RRULE storage,
next_occurrence_date, exceptions, timezone-aware expansion, and update-all-future semantics. - 25-33 minutes β Critical flows: walk create-with-invite, attendee response, range query, recurrence exception, cache invalidation, and WebSocket re-fetch after a change.
- 33-40 minutes β Deep dives: compare materialized occurrences with query-time expansion, overlap indexes with alternatives, read replicas/cache, and bitmap versus direct free/busy queries.
- 40-45 minutes β Reliability, security, and follow-ups: cover idempotent writes, outbox delivery, replica lag, recurrence-job retries, bitmap rebuilds, attendee authorization, privacy, DST, and multi-region trade-offs.
Core Entities
- Event: The core event record. Carries
event_id,creator_id,title,start_time,end_time,timezone,location,description, andrrule(null for one-off events, an RRULE string for recurring events). Schema details and indexes are deferred to the deep dives. - Attendee: The join between an event and a user. Carries
event_id,user_id, andstatus(pending,accepted,declined). One row per invited participant. - RecurrenceException: An override for one specific occurrence of a recurring event. Carries
base_event_id,original_occurrence_date,modified_event_id(points to a one-off event with the exception's fields), andis_deleted(true when the occurrence is cancelled rather than modified).
API Design
FR 1 and FR 2 - Create an event (with optional recurrence):
POST /events
Body: {
title: "Weekly sync",
start_time: "2026-04-07T10:00:00Z",
end_time: "2026-04-07T10:30:00Z",
timezone: "America/New_York",
rrule: "FREQ=WEEKLY;BYDAY=TU", // null for one-off events
attendees: ["user_456", "user_789"],
location: "Conference Room B"
}
Response: HTTP 201 Created
Body: { event_id: "evt_abc123" }
POST because the request creates a new resource. The rrule field follows the iCalendar RRULE spec (RFC 5545), which is a widely supported calendar interchange format. Returning event_id lets the client associate later change notifications with the event.
FR 4 - Get events in a time range:
GET /calendar/{user_id}/events?from=2026-01-01T00:00:00Z&to=2026-01-31T23:59:59Z
Response: HTTP 200 OK
Body: {
events: [
{
event_id: "evt_abc123",
title: "Weekly sync",
start_time: "2026-01-06T10:00:00Z",
end_time: "2026-01-06T10:30:00Z",
is_recurring_occurrence: true,
base_event_id: "evt_abc123"
}
]
}
The response returns expanded occurrences within the requested range, including the relevant occurrence times for recurring events. is_recurring_occurrence and base_event_id let the client know which events are instances of a recurring series so it can render them correctly.
FR 3 - Update attendee status:
PUT /events/{event_id}/attendees/{user_id}
Body: { status: "accepted" }
Response: HTTP 200 OK
PUT because the request updates a specific resource at a known URL. status is one of accepted or declined. The server writes to the Attendee table and notifies the event creator asynchronously.
Real-time updates (WebSocket):
WebSocket: ws://host/calendar/{user_id}/events
Server pushes: {
event_id: "evt_abc123",
change_type: "created" | "updated" | "deleted",
occurred_at: "2026-01-01T10:00:00Z"
}
The WebSocket channel scoped to user_id receives push notifications whenever any event on that user's calendar changes (including events where the user is an attendee). The client re-fetches the full event details using the REST API on receiving the notification.
High-Level Design
1. Creating and retrieving one-off events
A single app server backed by a relational database handles basic event creation and range queries, and the index on (user_id, start_time) is what makes those queries fast.
The simplest possible system: a client sends a POST /events request, the app server validates it and writes to a PostgreSQL table, and range queries hit a B-tree index on (user_id, start_time, end_time). No caching, no replication, no recurring events yet.
Components:
- Client: Web or mobile app that sends create/read requests.
- App Server: Validates request payload, writes the event to the database, reads events for range queries.
- PostgreSQL (Primary): Stores events with a composite index on
(user_id, start_time, end_time)for efficient range queries.
Request walkthrough (create):
- Client sends
POST /eventswith title, start time, end time, and timezone. - App Server validates that
end_time > start_timeand that required fields are present. - App Server inserts the event into the
eventstable with a generatedevent_id. - App Server returns
{ event_id }to the client.
Request walkthrough (range query):
- Client sends
GET /calendar/{user_id}/events?from=...&to=.... - App Server executes:
SELECT * FROM events
WHERE user_id = :user_id
AND start_time < :to
AND end_time > :from
ORDER BY start_time;
- App Server returns the matching events.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.