Think Beyond the Happy Path
Calendar systems aren't just about CRUD operations on events — they're about recurring event expansion, multi-device sync, and fast availability checking across complex schedules.
Before diving into the material, take a moment to ask yourself:
- Do you know how to handle recurring events — should you store every instance, or expand them on-the-fly? What happens when someone modifies "this and all future events"?
- Do you know how to support multi-device sync so changes appear instantly across phone, laptop, and web — what sync protocol would you use?
- Do you know how to quickly check availability and suggest meeting times for a group of 10 busy executives — without scanning millions of events?
- Do you know how to handle timezone edge cases like events that span DST transitions or users traveling across time zones?
You don't need to answer all of these right away. A Senior/Staff+ Engineer doesn't stop at basic functionality — they anticipate edge cases, design for resilience, and push for production-grade reliability.
But great systems start with great questions. What would you ask next?
Problem Statement
Design a calendar service that:
- Allows users to create, modify, and delete calendar events
- Supports invitations and RSVP tracking
- Provides calendar viewing with weekly/monthly views
- Enables free/busy availability checking across users
Functional Requirements
FR1 – Event Management (Create / Modify / Delete)
Users can create events on their calendars with basic details (title, time, all-day flag, description, location), update those details later, and delete events they own. Only the event organizer (or calendar owner) can modify or delete an event.
FR2 – Invitations and RSVP
Users can add guests to an event and the system records each guest’s response as Yes/No/Maybe. Guests can update their own response, and the organizer can always see the current RSVP status for all invitees.
FR3 – Calendar Viewing (Weekly by Default)
Users can view their schedule in a calendar UI, with the weekly view as the default. The weekly view shows all events for that week and allows users to navigate to other weeks, open event details, and start creating new events from empty time slots.
FR4 – Free/Busy and Availability Checking
Users can check other users’ availability over a given time window, seeing when they are free or busy without necessarily seeing event details. This free/busy view helps organizers choose suitable time slots for meetings while respecting each user’s visibility settings
Non-Functional Requirements
NFR1 – Strong Consistency (CAP: Consistency over Availability)
The calendar backend is a single source of truth: once an event or RSVP is saved, all clients should see the same canonical state. Under network partition or quorum loss, we prefer failing writes rather than accepting conflicting versions of the same event.
Why Consistency over Availability in this design?
- Impact of errors is worse than impact of failures: if an event silently “forks” (two times/guest lists both accepted), you get double-bookings and missed meetings. That feels like data corruption and destroys trust in the calendar. A transient “failed to save, please retry” is annoying but recoverable.
- Partitions are rare, events are core: DB/quorum partitions are exceptional; event correctness is exercised constantly. Optimizing for the rare case (AP) at the cost of everyday correctness (C) is the wrong trade here.
- Our other features depend on a single truth: free/busy computation, time suggestions, and multi-device sync all assume one canonical version of each event. If we allowed divergent writes, we’d need complex reconciliation logic that leaks into RSVP state, availability results, and device views.
- Offline is handled at the edge, not by weakening server guarantees: when a user is offline, we queue changes locally and reconcile on reconnect with version checks. We do not relax the server’s consistency just because clients may be disconnected; this keeps the core store clean and predictable.
NFR2 – Multi-Device Real-Time Sync
The same account can be signed in on multiple devices, and changes must stay aligned across them with predictable, near-real-time behavior.
- Cross-device propagation: Changes made on one online device appear on other online devices within 3–5 seconds (p95).
- Offline edits: Edits made offline are queued and reconciled on reconnect, with explicit conflict surfacing if a newer version exists.
NFR3 – High Scalability
The system must sustain large user bases and long event histories while maintaining performance as read-heavy traffic grows.
- Scale assumption: Up to tens of millions of users, each with hundreds of recurring series and thousands of events per year.
- Traffic profile: Optimized for reads ≫ writes, with strong spikes during business hours and meeting-heavy periods.
NFR4 – Low Latency
Common user interactions should feel instant, especially viewing schedules and checking availability.
- Views: Day/Week/Month view responses should be < 200 ms (p95).
- Availability: Free/busy queries for up to ~10 users over a 1–2 week window should be < 500 ms (p95).
APIs & Entities
Note on Structure – Why APIs & Entities Are Separate This Time?
In most ShowOffer delivery frameworks, we walk each FR vertically (FR → APIs → Entities → Workflow → Diagram). For this calendar system, I’ve intentionally pulled APIs and Entities into shared sections instead of repeating them inside each FR.
There are two main reasons:
-
Heavy cross-FR reuse of the same entities and APIs
In this problem, almost all FRs depend on the same core objects:
Event,EventException,Invitation,FreeBusyBlock, andChangeLog. The same APIs (e.g.,GET /v1/events,GET /v1/events?start_ts=&end_ts=,POST /v1/freebusy) are used by multiple FRs: FR1 for CRUD, FR3 for views, FR4 for free/busy. If we followed the standard per-FR layout, we’d either duplicate these definitions four times or constantly say “same as FR1,” which adds noise but no insight. -
Entities evolve as later FRs are introduced
The data model for a calendar is central and grows over time: FR1 defines
EventandEventException, FR2 addsInvitation, FR4 addsFreeBusyBlock, and all of them write toChangeLog. If entity tables lived inside each FR section, every refinement (e.g., addingrsvp_statusor changingrecurrence_rule) would require editing multiple places, increasing the risk of inconsistent diagrams and descriptions. A single Core Data Model section keeps entities canonical and lets FR sections focus on how they use those entities, not re-explain what they are.
Practically, this structure is still interview-friendly: verbally, I can walk each FR vertically (APIs → key entities → workflow), but in the written version I centralize APIs and Entities to avoid duplication and make it easier for the interviewer to see the big picture of the system at a glance.
Each FR maps to one or more API endpoints. Here we define signatures only, not full request/response payloads. Use the copy button to grab any endpoint. FR1 – Event Management (Create / Modify / Delete)
Create event
POST /v1/events
Creates a new event on the authenticated user's calendar. If recurrence_rule is omitted or null, this creates a one-time event; if recurrence_rule is provided (RRULE-style), this creates a recurring event series.
Get event
GET /v1/events/{event_id}
Returns full details of a single event.
Update event
PATCH /v1/events/{event_id}?scope=single|this_and_future|series
Updates event fields; scope controls how recurring events are affected.
Delete event
DELETE /v1/events/{event_id}?scope=single|this_and_future|series
Deletes/cancels an event; scope controls recurring behavior.
FR2 – Invitations and RSVP
Add or update invitees (organizer)
POST /v1/events/{event_id}/invitations
Adds new guests or updates the guest list for an event.
List invitees and RSVP status (organizer)
GET /v1/events/{event_id}/invitations
Returns all guests and their current RSVP status.
Update RSVP (guest)
PATCH /v1/events/{event_id}/rsvp
Authenticated user sets or updates their RSVP (Yes/No/Maybe) for the event.
FR3 – Calendar Viewing (Weekly by Default)
List events in a time range
GET /v1/events?start_ts={start}&end_ts={end}
Returns all events on the authenticated user's calendar within the given time window; the client uses this for weekly (default) and other views.
Get single event
GET /v1/events/{event_id}
(Same as FR1) Used when user clicks an event in the calendar view to see details.
FR4 – Free/Busy and Availability Checking
Free/busy for multiple users
POST /v1/availability
Given a list of user IDs and a time window, returns each user's busy intervals (no event details). This is used to power free/busy views in the UI and to manually choose suitable time slots for meetings.
Why POST not GET in checking availability?
Semantically it’s a read, so we could expose GET /v1/availability with query params, but in practice we’d use POST because the request shape (multiple users, time windows, optional constraints) fits a JSON body much better and avoids URL length / encoding headaches.
| UserRepresents an account in the system; the owner of exactly one calendar. | |
|---|---|
| Name | Description |
user_id | Primary key; unique identifier for the user. |
email | User's login / contact email. |
name | Display name. |
default_timezone | Default timezone for events and views. |
| CalendarRepresents the user's single personal calendar (1:1 with User). All /v1/events APIs operate on this calendar for the authenticated user. | |
|---|---|
| Name | Description |
calendar_id | Primary key; identifier for the calendar. |
owner_user_id | FK → User.user_id; unique (one calendar per user). |
name | Calendar display name (e.g., "My Calendar"). |
timezone | Default timezone for events on this calendar. |
| EventBacks all event CRUD APIs and calendar views (POST/GET/PATCH/DELETE /v1/events, GET /v1/events?start_ts&end_ts). A row can represent a single event or a recurring series; recurrence semantics and per-occurrence overrides are detailed in Deep Dive 1. | |
|---|---|
| Name | Description |
event_id | Primary key; identifier for the event or recurring series. |
calendar_id | FK → Calendar.calendar_id; calendar this event belongs to. |
organizer_user_id | FK → User.user_id; user who owns/organizes the event. |
title | Event title (e.g., "Team Sync"). |
description | Optional free-text notes. |
location | Optional location (room, address, link). |
start_ts | Start timestamp in UTC (base start for a one-off event or series). |
end_ts | End timestamp in UTC. |
is_all_day | Whether the event spans the whole day. |
timezone | Timezone for rendering this event in the UI. |
recurrence_rule | Nullable; when set, indicates this row is a recurring series (RRULE-style; full modeling in Deep Dive 1). |
status | ACTIVE or CANCELLED (soft delete / cancellation). |
version | Integer for optimistic concurrency on PATCH/DELETE. |
For recurring events, the Event row acts as the series definition. Per-occurrence overrides (e.g., "this instance only moved/cancelled") are modeled via additional tables introduced in Deep Dive 1 (e.g., EventException).
| InvitationRepresents each invitee (including guests) for an event, with their RSVP state; this is how FR2 APIs attach invitees to events. | |
|---|---|
| Name | Description |
invitation_id | Primary key; identifier for this invitation record. |
event_id | FK → Event.event_id; event the guest is invited to. |
guest_user_id | FK → User.user_id; invited user (nullable if external only). |
guest_email | Email of invited guest (used for external/lookup). |
rsvp_status | YES, NO, MAYBE, or PENDING. |
response_ts | Timestamp of the last RSVP update. |
is_organizer | Boolean; true if this row represents the organizer as an attendee. |
Organizer is on the Event; all invitees (including organizer, if we want) live in Invitation. That keeps invitee data normalized and lets FR2 evolve without bloating the Event row.
| FreeBusyBlock (Optional)In the simplest design, FR4 POST /v1/availability can compute free/busy directly from Event + Invitation by expanding relevant events in the requested window. At scale, we introduce a derived FreeBusyBlock store to precompute busy intervals per user and keep availability queries fast. | |
|---|---|
| Name | Description |
user_id | FK → User.user_id; user this busy block belongs to. |
start_ts | Start of busy interval in UTC. |
end_ts | End of busy interval in UTC. |
status | Busy type (e.g., BUSY, OOO, TENTATIVE). |
source | Optional reference (e.g., event_id or type) for debugging. |