Calendar / Reminder Service#
Problem statement (interviewer prompt)
Design Google Calendar: events with attendees, RSVP, recurring rules (RFC 5545 RRULE), time-zone correctness, sharing, free/busy queries, and reminder delivery (push + email) at the right local time for each attendee.
flowchart LR
U([User])
CAL[Calendar Service]
REM([Reminder Scheduler])
Q[(Delayed queue)]
NOTIF[Notification Service]
U --> CAL --> REM --> Q --> NOTIF --> U
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class U client;
class CAL,NOTIF service;
class Q datastore;
class REM compute;
flowchart TB
subgraph Apps
APP[Calendar app]
end
subgraph Edge
GW
end
subgraph Cal[Calendar]
CAL[Calendar Service]
EVT[(Events DB)]
RECUR[Recurrence expander - RFC 5545 RRULE]
INV[Invitees + RSVPs]
FREEBUSY[Free/busy lookup]
OOO[Out-of-office]
end
subgraph Sched[Reminder scheduler]
DELAYQ[[Delayed queue<br/>per-time bucket]]
WHEEL[Hierarchical timing wheel]
LEASE[Per-bucket lease]
DEDUP[Idempotency keys]
end
subgraph Notif
NS[Notification Service]
PUSH((APNS / FCM))
EMAIL
SMS
end
subgraph Sync
CALDAV[CalDAV / Google Calendar API]
PUSH_CHN[Realtime change channel]
end
Apps --> Edge --> Cal
Cal --> Sched --> Notif
Sync --- Cal
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class APP,CAL,RECUR,INV,FREEBUSY,OOO,WHEEL,DEDUP,NS,CALDAV,PUSH_CHN service;
class EVT datastore;
class DELAYQ queue;
class LEASE storage;
class PUSH external;
Recurrence#
- RFC 5545 RRULEs (e.g.,
FREQ=WEEKLY;BYDAY=MO,WE). - Expand on read or schedule per-occurrence ahead.
- Exceptions (deletes / modifications) stored separately.
Scheduler at scale#
- Time-bucketed queue: per-minute buckets.
- Workers lease a bucket at minute boundary; dispatch.
- For high-volume reminders, timing wheel with O(1) ops.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
LLD |
REST API design | verbs, statuses, pagination, errors | rest-api-design |
LLD |
Async models | futures / async-await / coroutines / actors | async-models |
LLD |
Error handling | exceptions vs Result, error boundaries | error-handling |
Quick reference#
Functional#
- Create events with start, end, attendees.
- Recurrence (RRULE).
- Reminders via push/email/SMS.
- Free/busy queries.
- Sync via CalDAV / Google API.
Non-functional#
- Sub-second reminder fire accuracy at minute granularity.
- 99.95% uptime.
Trade-offs#
- Expand recurrence on read vs materialize occurrences: hybrid - materialize next N occurrences for fast fan-out.
- Time-bucket queue vs timing wheel: wheel scales to billions of pending timers in memory.
Refs#
- RFC 5545 (iCalendar), RFC 4791 (CaldDAV).
- Google Calendar engineering posts.
- "Hashed and Hierarchical Timing Wheels" Varghese & Lauck.
FAQ#
How do you store recurring calendar events?#
Store the master event with an RFC 5545 RRULE (FREQ, INTERVAL, BYDAY) plus exception entries for individually edited instances. Materialize occurrences on read inside the requested window.
How does the reminder scheduler work?#
On event create or update, enqueue a delayed message with a deliver-at timestamp. A delayed queue or timer wheel releases it at the right moment to the notification service.
How do you handle time zones for reminders?#
Store the event in its native time zone and the user's preferred zone on their account. The scheduler converts at fire time so DST transitions resolve correctly.
How do you answer free/busy queries fast?#
Maintain a per-user busy-block index over a 90-day window, refreshed when events change. Free/busy is then a small range scan instead of expanding every recurring rule.
How do you scale to millions of calendars?#
Shard event storage by calendar_id so reads stay local. Use a separate fan-out service to materialize attendee notifications, since one event can fire reminders to thousands.