Difficulty: medium
Description: Design a calendar system like Google Calendar with event management, invitations, RSVP, and notifications
Introduction
Google Calendar is a comprehensive calendar management system that allows users to create, manage, and share calendar events. The system supports event creation, attendee invitations, RSVP functionality, and automated notifications. Users can view their calendars, create events, invite other users as attendees, and receive email notifications before events occur.
Key features include:
- Event Management: Create, read, update, and delete calendar events
- Attendee Management: Invite users to events and manage their RSVP status
- Notifications: Automated email reminders sent 30 minutes before events
- Access Control: Only calendar owners can modify events, attendees can RSVP
- Recurring Events: Support for recurring meeting patterns (deep dive topic)
Background
Unlike social media platforms—where user activity generates a high volume of frequent, lightweight writes such as likes, comments, and posts—Google Calendar prioritizes data correctness, integrity, and relationship modeling. Each calendar event may involve multiple users, resources, time constraints, notifications, and access controls, making consistency and structured interactions central to the system's design.
With the assumtion of 10 million daily active users (DAUs), the system exhibits a relatively low write frequency: assuming users create an average of 3 events per day, totaling around 30 million writes daily or ~347 write QPS. Each event is typically accessed multiple times—for viewing, notifications, or syncing—resulting in ~300 million reads per day, or ~3,472 read QPS. This is well within the capacity of modern cloud services. Therefore, we will be focusing on the entity, relationship, and the core features implementation.
Functional Requirements
Core Requirements
-
Event Retrieval: Users should be able to retrieve their calendar events for a specific time range. Only calendar owners can view all events in their calendar.
-
Event Creation and Modification: Calendar owners should be able to create, update, and delete events. When events are modified, the system should trigger notifications to attendees and update the notification scheduling system.
-
RSVP Management: Event attendees should be able to RSVP to events they're invited to. The system should validate that users can only RSVP to events where they are listed as attendees and should trigger notifications about RSVP changes.
-
Automated Notifications: The system should automatically send email notifications to all event attendees 30 minutes before each event starts. The notification system should be reliable and handle large volumes of notifications efficiently.
Out of Scope
- Multi-calendar support per user
- Calendar sharing and permissions beyond owner/attendee
- Time zone handling and conversion
- Advanced recurring event patterns
- Integration with external calendar systems
- Mobile push notifications (only email notifications in scope)
- Event conflict detection and resolution
Scale Requirements
- 10M Daily Active Users
- Event mutation is relatively infrequent (low QPS)
- Each user creates ~3 events per day on average
- Each event has ~5 attendees on average
- More focus on system design and logic than scale
- Data retention indefinitely
Non-Functional Requirements
- Consistency: Event data must be consistent across all users to prevent double-booking conflicts
- Security: Only authorized users can view/modify calendar events, with proper access control
- Scalability: System should handle millions of users and events efficiently
Database Schema
We don't typically go into too much detail about the database schema in system design. But for this problem, the schema is important to understand the relationships between the entities.
- users – application accounts (primary key
user_id
). - calendars – every owner has one (or more in the future) calendars; one-to-many (
owner_id → users
). - events – time-bounded items that live inside a calendar; many-to-one (
calendar_id → calendars
). - attendees – join table that captures the many-to-many relationship between users and events plus the
rsvp_status
attribute. RSVP status is a string that can beaccepted
,pending
, ordenied
. - notifications – records the notifications sent to attendees.
-- Users users ( user_id UUID PRIMARY KEY, email TEXT UNIQUE NOT NULL ); -- Calendar (one-to-many with Users) calendars ( calendar_id UUID PRIMARY KEY, owner_id UUID REFERENCES users(user_id) ); -- Events (belongs to a calendar) events ( event_id UUID PRIMARY KEY, calendar_id UUID REFERENCES calendars(calendar_id), created_by UUID REFERENCES users(user_id), start_time TIMESTAMP NOT NULL, end_time TIMESTAMP NOT NULL, title TEXT NOT NULL, description TEXT, location TEXT ); -- Attendees (many-to-many between users and events) attendees ( event_id UUID REFERENCES events(event_id), user_id UUID REFERENCES users(user_id), rsvp_status TEXT CHECK (rsvp_status IN ('accepted','pending','denied')), PRIMARY KEY (event_id, user_id) ); -- Notifications (emails to be sent) notifications ( notification_id UUID PRIMARY KEY, event_id UUID REFERENCES events(event_id), user_id UUID REFERENCES users(user_id), notify_at TIMESTAMP NOT NULL, sent BOOLEAN DEFAULT FALSE, sent_at TIMESTAMP );
The schema supports indexed queries on events.calendar_id
, events.start_time
, and composite (event_id, user_id)
in attendees
.
API Endpoints
GET /calendars/{calendarId}/events?start=start_time&end=end_time
Retrieve calendar events for a specific time range
Auth: Requires a JWT bearer token in the Authorization
header.
Allowed only if calendar.owner_id == token.sub
(the token's sub
claim is the authenticated user ID) since only calendar owners can view all events in their calendar.
Response Body:
{
"events": [
{
"event_id": "evt_123",
"title": "Team Meeting",
"description": "Weekly sync",
"start_time": "2024-01-15T14:00:00Z",
"end_time": "2024-01-15T15:00:00Z",
"location": "Conference Room A",
"attendees": [
{
"user_id": "user_456",
"rsvp_status": "accepted"
}
]
}
]
}
POST /calendars/{calendarId}/events
Create a new calendar event.
Automatically adds the creator (token.sub) to the attendees table with rsvp_status = "accepted"
so they always appear as attending their own event.
Auth: Only if calendar.owner_id == token.sub
Request Body:
{
"title": "Team Meeting",
"description": "Weekly sync",
"start_time": "2024-01-15T14:00:00Z",
"end_time": "2024-01-15T15:00:00Z",
"location": "Conference Room A",
"attendees": ["user_456", "user_789"]
}
Response Body:
{
"event_id": "evt_123",
"status": "created"
}
PATCH /calendars/{calendarId}/events/{eventId}
Update an existing calendar event
Auth: Only if calendar.owner_id == token.sub
Request Body:
{
"title": "Updated Team Meeting",
"description": "Weekly sync - updated agenda"
}
Response Body:
{
"event_id": "evt_123",
"status": "updated"
}
DELETE /calendars/{calendarId}/events/{eventId}
Delete a calendar event
Auth: Only if calendar.owner_id == token.sub
Response Body:
{
"status": "deleted"
}
POST /calendars/{calendarId}/events/{eventId}/rsvp
RSVP to an event
Auth: Only if token.sub
is in calendar.attendees
Request Body:
{
"rsvp_status": "accepted"
}
Response Body:
{
"status": "rsvp_updated"
}
High Level Design
1. Event Retrieval
Users should be able to retrieve their calendar events for a specific time range. Only calendar owners can view all events in their calendar.
Flow:
- Client issues
GET /calendars/{id}/events?start=&end=
. - API Gateway forwards the request to an Event-Service instance.
- Service authorizes the user, performs an indexed range query on
events
bycalendar_id
and time window, and returns the rows. - Client renders the calendar (result may be cached for subsequent loads).
2. Event Creation and Modification
Calendar owners should be able to create, update, and delete events. When events are modified, the system should trigger notifications to attendees and update the notification scheduling system.
Flow:
- Client sends
POST|PATCH|DELETE /calendars/{id}/events
. - Event-Service validates ownership, performs the DB write in a transaction, and commits.
- After commit it publishes an "event-changed" message.
- Notification workers consume the message to insert/update/delete reminder rows so attendees get the right email.
3. RSVP Management
Event attendees should be able to RSVP to events they're invited to. The system should validate that users can only RSVP to events where they are listed as attendees and should trigger notifications about RSVP changes.
Flow:
- Attendee submits
POST /events/{id}/rsvp
. - Event-Service validates attendee, updates
attendees.rsvp_status
, and emits anrsvp-changed
message. - Notification-Service consumes the message to email confirmations (and optionally the organizer).
4. Automated Notifications
The system should automatically send email notifications to all event attendees 30 minutes before each event starts. The notification system should be reliable and handle large volumes of notifications efficiently.
Flow:
- Scheduler periodically selects unsent rows where
notify_at <= now()
. - Notification-Service sends emails via provider and sets
sent=true
. - Failed sends are retried; repeated failures go to a dead-letter queue for manual review.
This is a very simplied job scheduler. Distributed job scheduler is quite involved and itself deserves a deep dive. We will be adding another design question for that.
Deep Dive Questions
How do you handle recurring events in the calendar system?
First, we need a way to define the recurring events. There are actually a well-defined standard for storing recurrence rules and open-source libraries to generate and parse them.
We can use the iCalendar RFC 5545 standard's RRULE to store the recurrence rule and use the rrule.js library to generate the recurrence rule.
We add a recurrence_rule
field to the Events table which is a string that stores the recurrence rule.
ALTER TABLE events ADD COLUMN recurrence_rule TEXT; -- Example values: -- "FREQ=WEEKLY;BYDAY=MO,WE,FR" (Monday, Wednesday, Friday weekly) -- "FREQ=MONTHLY;BYMONTHDAY=15" (15th of each month) -- "FREQ=DAILY;INTERVAL=2" (Every 2 days)
When loading calendar events, we need to query both non-recurring and recurring events.
- Query non-recurring events:
SELECT * FROM events
WHERE calendar_id = ?
AND recurrence_rule IS NULL
AND start_time >= ? AND start_time <= ?
- Query recurring events:
SELECT * FROM events
WHERE calendar_id = ?
AND recurrence_rule IS NOT NULL
AND start_time <= ? -- Only events that started before query end
Expand recurring events in application code:
Use libraries like rrule.js
(JavaScript) to generate instances:
const rrule = new RRule({
freq: RRule.WEEKLY,
byweekday: [RRule.MO, RRule.WE, RRule.FR],
});
And combine the results into a single list of events.
How do you implement conflict detection and find free time slots for scheduling?
Imagine we want to implement a feature that allows users to show their availability and find free time slots for scheduling.
Time ranges with events are merged into a single "busy" time range. Other users cannot see the event details but they know these time ranges cannot be used for scheduling.
To do this, we need to:
- Find if two events overlap
- Find time ranges between existing events that can be used for scheduling
Check if two events overlap
This is a straightforward SQL query. We can use the start_time
and end_time
of the events to check if they overlap.
SELECT COUNT(*) FROM events e
JOIN attendees a ON e.event_id = a.event_id
WHERE a.user_id = ?
AND a.rsvp_status = 'accepted'
AND e.start_time < ? -- proposed_end_time
AND e.end_time > ? -- proposed_start_time
Find time ranges between existing events
We want to merge overlapping events and find gaps between them. This is a classic greedy algorithm problem "merge intervals".
- Fetch all accepted events in time range:
SELECT start_time, end_time FROM events e
JOIN attendees a ON e.event_id = a.event_id
WHERE a.user_id = ? AND a.rsvp_status = 'accepted'
AND start_time >= ? AND end_time <= ?
ORDER BY start_time
- Merge overlapping intervals:
def merge_intervals(intervals):
if not intervals:
return []
merged = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1]:
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
else:
merged.append((start, end))
return merged
- Find gaps between merged intervals:
We can use the
merged
list to find gaps between the intervals.
Some things to note
Performance Optimization:
- Index on (user_id, start_time, end_time) for efficient conflict queries
- Cache frequently accessed user availability data
- Use database-level interval operations where supported
Business Rules:
- Allow double-booking (Google Calendar allows this)
- Provide conflict warnings rather than blocking
- Consider different event types (busy, free, tentative)
How do you handle concurrent event modifications when multiple users can edit the same event simultaneously?
So far we have assumed the owner of an event is the only one who can edit it. Now let's assume we want to implement a feature that allows multiple users to edit events. Inevitably, we will have to handle concurrent modifications.
Concurrent event modification is a classic distributed systems problem. When multiple users attempt to modify the same event simultaneously, we need to ensure data consistency while maintaining good user experience.
If the changes are non-conflicting, we can simply merge them. For example, if Alice changes the title and Bob changes the location, both changes can be applied.
If the changes are conflicting, we need to handle the conflict.
There are several approaches to handle conflicting changes:
- Reject and Retry: Simply reject the update and ask the user to try again.
- Last Writer Wins: Accept the latest changes and overwrite previous ones. Simple but can lose important data.
- User-Driven Resolution: Show both versions to the user and let them choose which changes to keep.
In the real-world, editing an event is a relatively infrequent operation but we do have to make sure a user whose change got overwritten by another user is notified right away. We choose the reject and retry approach and implement optimistic locking with version control to reject the second update.
Optimistic Locking with Version Control
Add a version field to the Events table:
ALTER TABLE events ADD COLUMN version INTEGER DEFAULT 1;
When a user loads an event, they receive the current version. When they attempt to save changes, we check if the version matches:
UPDATE events
SET title = ?, location = ?, version = version + 1
WHERE event_id = ? AND version = ?
If the version doesn't match (meaning someone else modified the event), the update fails and we return a conflict error to the user.
We cover Optimistic Locking in more detail in the Domain Knowledge section.
On the frontend, we can show a conflict error to the user and ask them to try again.