Build a scheduling platform where users can share availability, allow others to book meetings seamlessly, and integrate with external calendar systems while handling time zones and conflicts.
With roughly 50 availability checks for every 1 booking, this is a read-heavy system. That points us to two complementary stores: a relational database (PostgreSQL) as the durable source of truth for users and bookings, where we want strong consistency and transactions on the (relatively rare) writes, plus a Redis cache in front of it to absorb the huge volume of availability reads. Because writes are infrequent, paying for transactional guarantees on the write path does not hurt overall throughput, while caching keeps read latency low.
HOST GUEST (no account)
manage availability open booking link
| |
+--------------> [ API Gateway ] <-------+
(host auth; rate limiting + abuse protection on public pages)
|
+------------------------+------------------------+
| | |
[Availability Service] [Booking Service] [Calendar Integration]
| | | | |
[Rules] [Slot Cache] [Hold Manager] [Webhooks] [Rate Limiter]
[Engine] (Redis) | | ^
| v v |
| [Conflict Checker] [ Sync Queue ] |
v | (durable) |
[Users / Rules DB] <----- [Booking DB] --------> | |
(PostgreSQL) (PostgreSQL) v |
| [Sync Workers] ----+--> [External Calendars]
v (retry/backoff/DLQ)
[Notification Service] --> [Email / SMS Gateway]
Outbound: confirm booking -> write Booking DB -> enqueue sync job -> worker pushes to Google/Outlook
Inbound: provider webhook -> Sync Queue -> worker updates Calendar Events + invalidates availability cache
(Time zone/DST handled in-process via a standard time zone library inside Availability Service)Designing a robust notification system (scheduling, multi-channel delivery, retries, and time zone-aware reminders) is a large problem in its own right and could be its own design question. We keep the Notification Service in the high-level architecture because bookings trigger confirmations and reminders, but everything below this point deliberately does not design its internals.
PostgreSQL tables
- Users
user_id UUID PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, -- public handle, e.g. calendly.com/jane
email VARCHAR(255) UNIQUE NOT NULL,
time_zone VARCHAR(50) DEFAULT 'UTC',
availability_rules JSONB, -- the host's available time blocks
calendar_connections JSONB, -- encrypted Google/Outlook OAuth tokens
booking_preferences JSONB, -- min notice, max future booking
created_at TIMESTAMP DEFAULT NOW()
-- username & email are UNIQUE, so Postgres already indexes them
- Bookings
booking_id UUID PRIMARY KEY,
host_id UUID REFERENCES users(user_id),
guest_email VARCHAR(255) NOT NULL,
guest_name VARCHAR(255),
guest_timezone VARCHAR(50),
guest_answers JSONB, -- responses to the host's intake questions
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE,
status booking_status_enum DEFAULT 'confirmed', -- confirmed, cancelled
calendar_event_ids JSONB, -- external calendar event references
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(), -- updated on cancel
INDEX idx_host_bookings ON (host_id, start_time)
-- DB-level safety net: two CONFIRMED bookings for the same host
-- can never overlap in time, even if the Redis hold check is bypassed
EXCLUDE USING gist (
host_id WITH =,
tstzrange(start_time, end_time) WITH &&
) WHERE (status = 'confirmed')
- Calendar Events -- external busy times imported for conflict checks
event_id UUID PRIMARY KEY,
user_id UUID REFERENCES users(user_id),
external_event_id VARCHAR(255), -- Google/Outlook event ID
provider calendar_provider_enum, -- google, outlook, apple
start_time TIMESTAMP WITH TIME ZONE, -- busy block start
end_time TIMESTAMP WITH TIME ZONE, -- busy block end
last_synced TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, external_event_id, provider)
INDEX idx_busy_times ON (user_id, start_time, end_time)
- Sync Jobs (pending calendar syncs)
job_id UUID PRIMARY KEY,
booking_id UUID REFERENCES bookings(booking_id),
job_type VARCHAR(20), -- push_event, cancel_event
status VARCHAR(20) DEFAULT 'pending', -- pending, done, failed
attempts INT DEFAULT 0,
next_attempt_at TIMESTAMP, -- backoff schedule for retries
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_pending_jobs ON (status, next_attempt_at) -- workers poll this
Redis keys
- Availability Cache
Key: "avail:{user_id}:{date}"
Value: ["09:00-09:30", "10:00-10:30", "14:00-14:30"]
TTL: 24 hours
- Booking Holds
Key: "hold:{host_id}:{start_time}" -- keyed by host so the host can't be double-booked for that slot
Value: {"hold_id": "hold_8f3e2d1c", "guest_email": "client@example.com"}
TTL: 2 minutes (auto-expires if the guest does not confirm)GET /v1/availability/user_123?start_date=2025-01-15&end_date=2025-01-22&duration=30
{
"user_id": "user_123",
"time_zone": "America/New_York",
"available_slots": [
{
"date": "2025-01-15",
"slots": [
{"start": "09:00", "end": "09:30"},
{"start": "10:00", "end": "10:30"},
{"start": "14:00", "end": "14:30"}
]
},
{
"date": "2025-01-16",
"slots": [
{"start": "09:30", "end": "10:00"},
{"start": "15:00", "end": "15:30"}
]
}
],
"booking_rules": {
"min_notice_hours": 24,
"max_future_days": 60
}
}{
"host_id": "user_123",
"start_time": "2025-01-15T09:00:00-05:00",
"end_time": "2025-01-15T09:30:00-05:00",
"guest_email": "client@example.com",
"guest_name": "Jane Smith"
}{
"hold_id": "hold_8f3e2d1c",
"status": "held",
"expires_at": "2025-01-14T10:32:00Z",
"booking_url": "https://calendly.com/confirm/hold_8f3e2d1c"
}{
"hold_id": "hold_8f3e2d1c",
"guest_details": {
"name": "Jane Smith",
"email": "client@example.com",
"phone": "+1-555-0123",
"notes": "Discussing Q1 project requirements"
},
"meeting_preferences": {
"location": "Google Meet",
"send_reminders": true,
"reminder_times": [24, 1] // hours before
}
}{
"booking_id": "booking_7a2b8f4e",
"status": "confirmed",
"meeting_details": {
"start_time": "2025-01-15T09:00:00-05:00",
"end_time": "2025-01-15T09:30:00-05:00",
"location": "https://meet.google.com/abc-defg-hij",
"calendar_event_id": "google_cal_event_123"
},
"confirmation_sent": true,
"reminder_scheduled": true
}{
"working_hours": {
"monday": {"start": "09:00", "end": "17:00"},
"tuesday": {"start": "09:00", "end": "17:00"},
"friday": {"start": "09:00", "end": "15:00"}
},
"meeting_preferences": {
"default_duration": 30,
"min_notice_hours": 24,
"max_future_days": 60
}
}{
"user_id": "user_123",
"availability_updated": true,
"cache_invalidated": true,
"affected_bookings": [],
"next_available_slot": "2025-01-15T09:00:00-05:00"
}DELETE /v1/bookings/booking_7a2b8f4e
{
"booking_id": "booking_7a2b8f4e",
"status": "cancelled",
"cancelled_at": "2025-01-14T10:45:00Z",
"calendar_events_removed": [
{"provider": "google", "event_id": "google_cal_event_123"},
{"provider": "outlook", "event_id": "outlook_event_456"}
],
"notifications_sent": {
"host_notified": true,
"guest_notified": true,
"cancellation_email_sent": true
},
"slot_released": true
}