Design a Meeting Calendar System¶
Interview Time: 60 min | Difficulty: Medium
Key Focus: Conflict detection, booking atomicity, recurring events
Step 1: Functional & Non-Functional Requirements¶
Functional Requirements¶
- Users create calendar accounts with timezone
- Create, update, delete events (title, time, location, attendees)
- Automatic conflict detection (double-booking prevention)
- Support recurring events (daily, weekly, monthly)
- Invite attendees, track RSVP (accept, decline, tentative)
- Share event calendar with colleagues
- Calendar permissions (view, edit, delegate)
- Find common available time slots for group meetings
- Integration with email (send invites, reminders)
- Timezone conversion for distributed teams
Non-Functional Requirements¶
| Requirement | Target | Notes |
|---|---|---|
| Scale | 1B users, 10B events/month | High write load |
| Latency | Create event <100ms, conflict check <50ms | Real-time feedback |
| Availability | 99.95% uptime | Always accessible |
| Consistency | Strong for conflict detection | No double-booking |
| Throughput | 100K event creates/sec | Peak hours |
| Query Speed | Free slot search <200ms | For scheduling assistants |
Step 2: API Design, Data Model & High-Level Design¶
Core API Endpoints¶
# Event Management
POST /calendars/{calendar_id}/events
{title, start_time, end_time, attendees, timezone, recurrence_rule?}
→ {event_id, conflicts: []}
GET /calendars/{calendar_id}/events?start=date&end=date
→ {events: [{event_id, title, start_time, duration}]}
PUT /calendars/{calendar_id}/events/{event_id}
{title?, start_time?, end_time?, attendees?}
→ {success, new_conflicts: []}
DELETE /calendars/{calendar_id}/events/{event_id}
→ {deleted: true}
# Conflict Detection
POST /calendars/{calendar_id}/check-availability
{start_time, end_time, required_attendees: [user_ids]}
→ {available: true/false, conflicts: [{user, event_title, time}]}
# Free Slot Search
POST /calendars/{calendar_id}/find-slots
{duration_minutes, attendees: [user_ids], date_range: {start, end}}
→ {slots: [{start_time, end_time, available_count}]}
# RSVP
POST /events/{event_id}/rsvp
{user_id, response: accept|decline|tentative}
→ {rsvp_recorded, updated_count}
# Recurring Events
POST /calendars/{calendar_id}/recurring-events
{title, start_time, end_time, recurrence: {frequency: daily|weekly, interval: 1}}
→ {series_id, events_created: 100}
Entity Data Model¶
CALENDARS
├─ calendar_id (ULID, PK)
├─ user_id (FK)
├─ name (e.g., "Work Calendar")
├─ timezone
├─ is_primary (one per user)
├─ created_at
EVENTS
├─ event_id (ULID, PK, sortable by time)
├─ calendar_id (FK, partition key)
├─ organizer_id (FK)
├─ title, description
├─ start_time (TIMESTAMP, indexed)
├─ end_time (TIMESTAMP)
├─ location
├─ event_type (meeting, task, blocking)
├─ status (scheduled, cancelled, rescheduled)
├─ recurrence_rule (nullable, e.g., "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR")
├─ series_id (for recurring events, group them)
├─ is_all_day (boolean)
├─ timezone
├─ created_at, updated_at
EVENT_ATTENDEES
├─ attendee_id (PK)
├─ event_id (FK)
├─ user_id (FK)
├─ response (accepted, declined, tentative, awaiting)
├─ responded_at (nullable)
├─ is_organizer (boolean)
├─ can_edit (boolean, permissions)
CALENDAR_BLOCKING
├─ blocking_id (PK)
├─ event_id (FK)
├─ blocked_user_ids [array] (users this event blocks)
├─ block_type (conflict, tentative, pto)
RECURRENCE_RULES
├─ rule_id (PK)
├─ event_id (FK)
├─ frequency (DAILY, WEEKLY, MONTHLY, YEARLY)
├─ interval (every N occurrences)
├─ day_of_week [MON, TUE, WED, ...] (for weekly)
├─ end_date (nullable, infinite if null)
├─ exception_dates [dates to skip] (for individual overrides)
High-Level Architecture¶
graph TB
USER["👤 User<br/>(creates event)"]
API["API Gateway<br/>(event CRUD)"]
CONFLICT["Conflict Detection<br/>(check overlaps)"]
DB["PostgreSQL<br/>(time-series partitioned<br/>by calendar_id, start_time)"]
CACHE["Redis<br/>(user busy hours,<br/>recurrence expansion)"]
SEARCH["Elasticsearch<br/>(full-text search)"]
QUEUE["Message Queue<br/>(event invites,<br/>reminders)"]
EMAIL["Email Service<br/>(send invites)"]
AVAILABILITY["Availability Service<br/>(find free slots)"]
USER --> API
API --> CONFLICT
CONFLICT --> DB
CONFLICT --> CACHE
API --> DB
API --> SEARCH
API --> QUEUE
QUEUE --> EMAIL
API --> AVAILABILITY
AVAILABILITY --> DB
AVAILABILITY --> CACHE
Step 3: Concurrency, Consistency & Scalability¶
🔴 Problem: Double-Booking Race Condition¶
Scenario: User has 2pm-3pm free. Two invites arrive simultaneously for overlapping times. Both get accepted!
Solution: Atomic Conflict Check + Lock
Conflict Detection (Atomic Transaction):
1. User receives invite: 2:30pm - 3:30pm Friday June 1st
2. Database transaction (SERIALIZABLE isolation):
BEGIN TRANSACTION
-- Check for conflicts
SELECT * FROM events
WHERE calendar_id = user_cal_123
AND start_time < '2030pm'
AND end_time > '2:30pm'
AND status = 'scheduled'
FOR UPDATE SKIP LOCKED; -- Lock matching rows
IF conflict_count > 0:
ROLLBACK, return conflict error
ELSE:
INSERT INTO events (...)
COMMIT
Result:
- Database serializes concurrent requests
- Only one CREATE succeeds
- Other gets "conflict detected" error
- No double-booking possible
Performance:
- Lock only affects 1 small time range
- Other time slots unaffected
- For busy person: many small locks, not one big lock
🟡 Problem: Recurring Event Expansion Performance¶
Scenario: "Weekly team meeting for 2 years" = 104 events. If on-the-fly, 104 DB queries for conflict check. Slow!
Solution: Smart Expansion + Caching
Recurring Event Handling:
1. Storage (efficient):
EVENTS table:
{
event_id: evt_001,
series_id: series_xyz, -- Groups all occurrences
start_time: "2024-06-01 2:00pm",
recurrence_rule: "FREQ=WEEKLY;BYDAY=FR",
expansion_end: "2026-06-01"
}
NOT stored:
- Don't create 104 separate rows
- Store only master + recurrence rule
2. Conflict Check (on-the-fly expansion):
User tries to book new event: June 1, 3:00pm - 3:30pm
Query:
SELECT * FROM events
WHERE series_id = series_xyz
AND expand_recurrence(recurrence_rule, date_range)
INTERSECTS (3:00pm, 3:30pm)
expand_recurrence():
For FREQ=WEEKLY;BYDAY=FR from June 1-30:
Friday June 1: 2:00pm
Friday June 8: 2:00pm
Friday June 15: 2:00pm
Friday June 22: 2:00pm
Friday June 29: 2:00pm
Return: all have 2:00pm-3:00pm (don't conflict with 3:00pm-3:30pm)
3. Cache expansion (for fast repeats):
Key: "recurring:{series_id}:{month}"
Value: ["2024-06-01 2:00pm", "2024-06-08 2:00pm", ...]
TTL: 30 days
→ Avoid re-expanding same series multiple times
🔷 Problem: Timezone Complexity for Distributed Teams¶
Scenario: 10am PST meeting. It's 1pm EST for East Coast, 6pm GMT for London. When recurring weekly: 10am PST stays fixed (even with DST changes).
Solution: Store Times in UTC, Apply Timezone on Read
Timezone Handling:
1. Storage (UTC only):
Event: "Weekly standup"
Organizer timezone: America/Los_Angeles
start_time: 2024-06-01 17:00:00 UTC (= 10am PST)
end_time: 2024-06-01 17:30:00 UTC
2. User views calendar (apply timezone on display):
IF user_timezone = "America/New_York":
Display: 1:00pm EST
ELSE IF user_timezone = "Europe/London":
Display: 6:00pm GMT
3. Recurring logic respects organizer's timezone:
Organizer sets: "Every Friday 10am PST"
Recurrence rule stored with timezone:
{
frequency: WEEKLY,
byday: FR,
time: 10:00:00,
timezone: America/Los_Angeles -- Key point!
}
Friday expansion:
- Compute day in organizer's timezone
- Convert to UTC for storage
- Each Friday: [2024-06-01 17:00 UTC, 2024-06-08 17:00 UTC, ...]
Even with DST transitions (Mar 10, Nov 3):
- Still "10am PST" in organizer's view
- But UTC time shifts by 1 hour
- All attendees see consistent absolute time
Step 4: Persistence Layer, Caching & Monitoring¶
Database Design¶
CREATE TABLE calendars (
calendar_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
name VARCHAR(255),
timezone VARCHAR(50),
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE events (
event_id BIGSERIAL PRIMARY KEY,
calendar_id BIGINT NOT NULL REFERENCES calendars(calendar_id),
organizer_id BIGINT NOT NULL,
series_id BIGINT, -- for recurring events
title VARCHAR(255),
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
timezone VARCHAR(50),
recurrence_rule TEXT, -- RRULE format
status VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_events_calendar_start
ON events(calendar_id, start_time DESC);
CREATE INDEX idx_events_calendar_time_range
ON events(calendar_id, start_time, end_time);
CREATE INDEX idx_events_series
ON events(series_id);
CREATE TABLE event_attendees (
attendee_id BIGSERIAL PRIMARY KEY,
event_id BIGINT NOT NULL REFERENCES events(event_id),
user_id BIGINT NOT NULL,
response VARCHAR(50), -- accepted, declined, tentative
responded_at TIMESTAMP,
can_edit BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_attendees_user_response
ON event_attendees(user_id, response);
Caching Strategy¶
Redis Caching:
1. User Busy Hours (quick conflict check)
Key: "calendar:{calendar_id}:busy:{date}"
Value: [(start_time, end_time), ...]
TTL: 1 day
Hit rate: 80% (same days checked repeatedly)
2. Recurring Expansion (avoid recomputing)
Key: "recurring:{series_id}:{month}"
Value: [timestamp, timestamp, ...] (expanded occurrences)
TTL: 30 days
3. Free Slot Cache (for scheduling assistants)
Key: "free_slots:{user_ids_hash}:{date_range}"
Value: [{start, end, available_count}]
TTL: 1 hour (people book constantly)
Monitoring¶
- alert: ConflictCheckLatencyHigh
expr: conflict_check_latency_p95 > 100 # 100ms
annotations: "Conflict detection slow — check DB query"
- alert: DoubleBookingDetected
expr: double_booking_count > 0
annotations: "Double-booking detected — transaction isolation issue!"
- alert: CalendarQueryTimeout
expr: calendar_query_timeout_count > 100
annotations: "Too many queries timing out — need index or caching"
- alert: RecurrenceExpansionSlow
expr: recurrence_expansion_latency_p95 > 200
annotations: "Recurring event expansion slow — check cache hit rate"
⚡ Quick Reference Cheat Sheet¶
Critical Decisions¶
- SERIALIZABLE isolation — Prevents double-booking via transaction locks
- UTC storage — Timezone applied on read, not stored
- Series-based recurrence — One row + rule, not 104 rows
- Expansion caching — Pre-compute recurring instances for speed
- Busy hours cache — Quick conflict check before DB query
Tech Stack¶
API: Go/Node.js (low-latency)
Database: PostgreSQL (ACID, time-series)
Cache: Redis (busy hours, expansions)
Search: Elasticsearch (event search)
Scheduler: iCalendar RFC 5545 (RRULE format)
Email: SendGrid (invites, reminders)
🎯 Interview Summary (5 Minutes)¶
- Atomic conflict check → SERIALIZABLE transaction + row-level locks
- Recurring optimization → Store series + rule, expand on query (cache result)
- Timezone handling → Store UTC, apply timezone on read
- Busy hour caching → Pre-compute by date, 80% hit rate
- Free slot search → Expand recurring, find gaps, return sorted slots
- RSVP tracking → Separate attendees table, track response state
- Conflict notification → Queue + email service for async invites