system-design21 min read

Design Google Calendar — System Design Deep Dive | Ayodhyya

Chapter 8: Design Google Calendar

Building a globally distributed calendar system with event scheduling, conflict detection, recurring events, multi-timezone support, and real-time notifications

Ayodhyya · System Design Deep Dive Series · Target Audience: Senior/Staff/Principal Engineers

Table of Contents

  1. Introduction
  2. Functional and Non-Functional Requirements
  3. Capacity Estimation
  4. Data Model
  5. High-Level Architecture
  6. Event Scheduling and Conflict Detection
  7. Recurring Events
  8. Timezone Handling
  9. Notification System
  10. Free/Busy Lookup
  11. Calendar Sharing and Permissions
  12. Sync Protocol
  13. Read/Write Paths
  14. Failure Scenarios
  15. Cost Estimation
  16. Interview Questions and Answers

1. Introduction

Google Calendar is one of the most widely used productivity applications in the world, serving over 1.5 billion users across web, mobile, and wearable platforms. Behind its deceptively simple interface lies a complex distributed system that must handle event scheduling across timezones, detect conflicts, support recurring events with complex recurrence rules, deliver timely notifications, synchronize state across multiple devices, and manage calendar sharing with fine-grained permissions.

The fundamental challenge of a calendar system is managing state correctly across multiple participants, timezones, and devices. A meeting scheduled in New York must display correctly for participants in London, Tokyo, and Sydney. Recurring events must handle edge cases like daylight saving time transitions, leap years, and modified instances. Calendar sharing must support different permission levels while maintaining low-latency read access.

Why This Is Hard: Calendar systems must solve several hard distributed systems problems simultaneously: (1) conflict detection requires strong consistency for overlapping event checks, (2) recurring event expansion requires complex date arithmetic across timezones, (3) notification delivery must be reliable and timely (missing a meeting reminder is worse than a late one), and (4) multi-device sync must handle offline edits and resolve conflicts gracefully.

Real-World Scale

MetricValueNotes
Monthly active users1.5 billionAcross all platforms
Events created per day~500 millionPeak during business hours
Average events per user~15/monthVaries by region and profession
Concurrent connections~100 millionPeak during business hours
Notifications per day~2 billionReminders, invitations, updates
Timezones supported~100IANA timezone database
Recurrence rules (RRULE)~30% of eventsDaily, weekly, monthly, custom

2. Functional and Non-Functional Requirements

Functional Requirements

  1. Create/Edit/Delete events: Users can create events with title, description, time, location, attendees, and reminders.
  2. Recurring events: Support RRULE-based recurrence patterns (daily, weekly, monthly, yearly, custom).
  3. Conflict detection: Detect and prevent double-booking when creating or modifying events.
  4. Free/Busy lookup: Check availability of users before scheduling meetings.
  5. Calendar sharing: Share calendars with varying permission levels (free/busy only, limited details, full access).
  6. Notifications: Push notifications, email reminders, and in-app alerts for upcoming events.
  7. Multi-device sync: Real-time synchronization across web, mobile, and desktop clients.
  8. Multi-timezone support: Events display correctly in the user's local timezone.
  9. Attachments and location: Support file attachments and location data (including map integration).

Non-Functional Requirements

RequirementTargetJustification
Latency (p99)<200ms for readsCalendar loads must feel instant
Latency (p99)<500ms for writesEvent creation should feel responsive
Availability99.99%Calendar is critical for business operations
Durability100%Never lose calendar data
Notification delivery<30 secondsReminders must arrive on time
Sync latency<5 secondsChanges visible across devices quickly
Conflict detectionStrong consistencyMust prevent double-booking

3. Capacity Estimation

public class CalendarCapacityEstimator
{
    public static void Estimate()
    {
        // Traffic
        long monthlyActiveUsers = 1_500_000_000L;
        long eventsPerDay = 500_000_000L;
        double readsPerEventPerDay = 5.0;   // User views event multiple times
        long readsPerDay = eventsPerDay * (long)readsPerEventPerDay + monthlyActiveUsers * 2;
        long writesPerDay = eventsPerDay * 3; // Create + edits + RSVPs
        long notificationsPerDay = 2_000_000_000L;

        Console.WriteLine($"Reads/day: {readsPerDay:N0}");
        Console.WriteLine($"Writes/day: {writesPerDay:N0}");
        Console.WriteLine($"Notifications/day: {notificationsPerDay:N0}");

        // Storage
        long avgEventSizeBytes = 2048;  // Title, description, attendees, metadata
        long avgEventsPerUser = 500;     // Lifetime events per user
        double totalEventsStorageGB = (monthlyActiveUsers * avgEventsPerUser * avgEventSizeBytes) / (1024.0 * 1024 * 1024);
        Console.WriteLine($"Total event storage: {totalEventsStorageGB:N0} GB");

        // Recurrence expansion storage
        long avgRecurrenceInstances = 52; // Average recurring instances per rule
        double recurrenceStorageGB = (eventsPerDay * 0.3 * avgRecurrenceInstances * 100) / (1024.0 * 1024 * 1024);
        Console.WriteLine($"Recurrence instance storage: {recurrenceStorageGB:N0} GB");
    }
}
MetricValue
Reads per second~80K (average), ~250K (peak)
Writes per second~17K (average), ~50K (peak)
Notifications per second~23K (average), ~70K (peak)
Total event storage~1.4 PB
Recurrence instance storage~15 TB
Attachment storage~500 TB
Notification queue capacity~2B messages/day

4. Data Model

public class Calendar
{
    public string CalendarId { get; set; }
    public string OwnerUserId { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public string Color { get; set; }
    public CalendarVisibility Visibility { get; set; }  // Default, Public, Private
    public CalendarRole DefaultRole { get; set; }        // Reader, Writer, Owner
    public List<CalendarACL> ACLs { get; set; }         // Access control list
    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset UpdatedAt { get; set; }
    public long ETag { get; set; }                        // Optimistic concurrency control
}

public class CalendarEvent
{
    public string EventId { get; set; }
    public string CalendarId { get; set; }
    public string Summary { get; set; }           // Title
    public string Description { get; set; }
    public string Location { get; set; }
    public GeoLocation GeoLocation { get; set; }
    public EventStatus Status { get; set; }       // Confirmed, Tentative, Cancelled
    public bool IsAllDay { get; set; }
    public DateTimeOffset StartTime { get; set; }
    public DateTimeOffset EndTime { get; set; }
    public string StartTimezone { get; set; }     // IANA timezone (e.g., "America/New_York")
    public string EndTimezone { get; set; }
    public List<EventAttendee> Attendees { get; set; }
    public List<EventReminder> Reminders { get; set; }
    public RecurrenceRule RRULE { get; set; }     // Null for non-recurring events
    public string RecurringEventId { get; set; }  // Links instances to parent
    public string OriginalStartTime { get; set; } // For modified recurring instances
    public List<string> AttachmentIds { get; set; }
    public string OrganizerId { get; set; }
    public CalendarVisibility Visibility { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
    public DateTimeOffset UpdatedAt { get; set; }
    public long ETag { get; set; }
    public Dictionary<string, string> ExtendedProperties { get; set; }
}

public class EventAttendee
{
    public string UserId { get; set; }
    public string Email { get; set; }
    public string DisplayName { get; set; }
    public RSVPStatus ResponseStatus { get; set; } // NeedsAction, Declined, Tentative, Accepted
    public bool IsOptional { get; set; }
    public bool IsOrganizer { get; set; }
}

public class EventReminder
{
    public ReminderMethod Method { get; set; } // Pop, Email, Sms
    public int MinutesBeforeEvent { get; set; }
}

public class RecurrenceRule
{
    public string RRULE { get; set; }          // RFC 5545 RRULE string
    public List<string> EXDATE { get; set; }   // Exclusion dates
    public List<string> RDATE { get; set; }    // Additional dates
    public DateTimeOffset? UNTIL { get; set; }  // End date for recurrence
    public int? COUNT { get; set; }             // Max number of occurrences
}

public class FreeBusyInfo
{
    public string UserId { get; set; }
    public List<TimeSlot> BusySlots { get; set; }
    public List<TimeSlot> TentativeSlots { get; set; }
}

Database Schema (Cassandra/Bigtable)

TablePartition KeySort KeyUse Case
events_by_calendarcalendar_idstart_time, event_idLoad calendar view
events_by_useruser_iddate, event_idUser's aggregated view
free_busy_by_useruser_iddate_rangeFree/busy lookup
recurrence_instancesrecurring_event_idinstance_dateRecurring event instances
notification_queuenotification_datesend_time, user_idScheduled notifications
calendar_aclscalendar_idgrantee_idPermission checks

5. High-Level Architecture

Google Calendar System Architecture

graph TB subgraph "Client Layer" WEB[Web Client] MOB[Mobile Apps] API[Third-party API] end subgraph "API Gateway" GW[API Gateway / Load Balancer] end subgraph "Core Services" GW --> ES[Event Service] GW --> NS[Notification Service] GW --> CS[Calendar Sync Service] GW --> FB[Free/Busy Service] end subgraph "Business Logic" ES --> CD[Conflict Detector] ES --> RE[Recurrence Engine] ES --> TZ[Timezone Service] end subgraph "Data Layer" ES --> DB[(Event Database)] ES --> CACHE[Redis Cache] NS --> NQ[Notification Queue] CS --> CSDB[(Sync Token Store)] FB --> FBDB[(Free/Busy Store)] end subgraph "Background Jobs" NQ --> ND[Notification Dispatcher] RE --> RIE[Recurrence Instance Expander] ND --> EMAIL[Email Service] ND --> PUSH[Push Notification Service] ND --> SMS[SMS Service] end style GW fill:#3b82f6 style ES fill:#6366f1 style NS fill:#f59e0b style DB fill:#22c55e

Service Responsibilities

ServiceResponsibilityScaling Strategy
Event ServiceCRUD for events, conflict detectionSharded by calendar_id, horizontally scaled
Notification ServiceReminders, invitations, updatesAsync via message queue, partitioned by user
Calendar Sync ServiceMulti-device synchronizationLong-polling + SSE, stateless
Free/Busy ServiceAvailability queries for schedulingPrecomputed daily aggregates, cached
Recurrence EngineExpand RRULE into instancesBatch job + on-demand expansion
Timezone ServiceDST transitions, timezone conversionsStatic data, heavily cached

6. Event Scheduling and Conflict Detection

Conflict Detection Algorithm

Detecting scheduling conflicts requires checking whether a new event overlaps with any existing event on the same calendar (or across shared calendars). This must be atomic to prevent race conditions.

public class ConflictDetector
{
    private readonly IEventStore _eventStore;
    private readonly IDistributedLock _lockManager;

    public async Task<ConflictResult> CheckConflictsAsync(
        string calendarId, DateTimeOffset start, DateTimeOffset end,
        string excludeEventId = null)
    {
        // Use a distributed lock on the calendar to prevent concurrent modifications
        var lockKey = $"calendar-lock:{calendarId}";
        using var lockHandle = await _lockManager.AcquireAsync(lockKey, TimeSpan.FromSeconds(5));

        // Query all events in the time range for this calendar
        var existingEvents = await _eventStore.GetEventsInRangeAsync(
            calendarId, start.AddDays(-1), end.AddDays(1));

        var conflicts = new List<CalendarEvent>();

        foreach (var existing in existingEvents)
        {
            // Skip the event being modified
            if (existing.EventId == excludeEventId) continue;

            // Skip cancelled events
            if (existing.Status == EventStatus.Cancelled) continue;

            // Check for time overlap
            if (TimesOverlap(start, end, existing.StartTime, existing.EndTime))
            {
                conflicts.Add(existing);
            }
        }

        return new ConflictResult
        {
            HasConflicts = conflicts.Count > 0,
            ConflictingEvents = conflicts,
            Suggestion = conflicts.Count > 0
                ? FindNextAvailableSlot(calendarId, start, end - start, existingEvents)
                : null
        };
    }

    private bool TimesOverlap(DateTimeOffset start1, DateTimeOffset end1,
        DateTimeOffset start2, DateTimeOffset end2)
    {
        return start1 < end2 && start2 < end1;
    }

    private DateTimeOffset? FindNextAvailableSlot(string calendarId,
        DateTimeOffset desiredStart, TimeSpan duration, List<CalendarEvent> existing)
    {
        var sorted = existing.OrderBy(e => e.StartTime).ToList();
        var candidate = desiredStart;

        foreach (var evt in sorted)
        {
            if (candidate + duration <= evt.StartTime)
                return candidate; // Found a slot before this event

            if (candidate < evt.EndTime)
                candidate = evt.EndTime; // Move past this event
        }

        return candidate; // Available after all events
    }
}

Optimistic Concurrency with ETags

public class EventService
{
    private readonly IEventStore _eventStore;
    private readonly ConflictDetector _conflictDetector;
    private readonly IEventBus _eventBus;

    public async Task<CalendarEvent> CreateEventAsync(CreateEventRequest request)
    {
        // Validate inputs
        ValidateEventRequest(request);

        // Check for conflicts
        var conflictResult = await _conflictDetector.CheckConflictsAsync(
            request.CalendarId, request.StartTime, request.EndTime);

        if (conflictResult.HasConflicts)
        {
            throw new SchedulingConflictException(
                "Event conflicts with existing events",
                conflictResult.ConflictingEvents,
                conflictResult.Suggestion);
        }

        // Create the event
        var evt = new CalendarEvent
        {
            EventId = Guid.NewGuid().ToString("N"),
            CalendarId = request.CalendarId,
            Summary = request.Summary,
            Description = request.Description,
            Location = request.Location,
            StartTime = request.StartTime,
            EndTime = request.EndTime,
            StartTimezone = request.StartTimezone,
            EndTimezone = request.EndTimezone,
            Attendees = request.Attendees,
            Reminders = request.Reminders,
            RRULE = request.RecurrenceRule,
            Status = EventStatus.Confirmed,
            CreatedAt = DateTimeOffset.UtcNow,
            UpdatedAt = DateTimeOffset.UtcNow,
            ETag = 1
        };

        await _eventStore.SaveAsync(evt);

        // Publish event created notification
        await _eventBus.PublishAsync(new EventCreatedNotification
        {
            Event = evt,
            CalendarId = request.CalendarId,
            NotifyAttendees = request.NotifyAttendees
        });

        return evt;
    }

    public async Task<CalendarEvent> UpdateEventAsync(
        string eventId, UpdateEventRequest request, long expectedETag)
    {
        var existing = await _eventStore.GetAsync(eventId);
        if (existing == null) throw new EventNotFoundException(eventId);

        // Optimistic concurrency check
        if (existing.ETag != expectedETag)
            throw new ConcurrentModificationException(
                "Event was modified by another user. Please refresh and try again.");

        // Check conflicts for new time range (if time changed)
        if (request.StartTime != existing.StartTime || request.EndTime != existing.EndTime)
        {
            var conflictResult = await _conflictDetector.CheckConflictsAsync(
                existing.CalendarId, request.StartTime ?? existing.StartTime,
                request.EndTime ?? existing.EndTime, excludeEventId: eventId);

            if (conflictResult.HasConflicts)
                throw new SchedulingConflictException("Updated time conflicts with existing events",
                    conflictResult.ConflictingEvents);
        }

        // Apply updates
        ApplyUpdates(existing, request);
        existing.UpdatedAt = DateTimeOffset.UtcNow;
        existing.ETag++;

        await _eventStore.SaveAsync(existing);

        await _eventBus.PublishAsync(new EventUpdatedNotification
        {
            Event = existing,
            Changes = GetChangedFields(existing, request)
        });

        return existing;
    }
}

7. Recurring Events

RRULE Expansion

Recurring events are defined using RFC 5545 RRULE syntax. The system must expand recurrence rules into individual instances while handling edge cases like DST transitions, exclusion dates, and modified instances.

public class RecurrenceExpander
{
    public List<CalendarEvent> ExpandRecurrence(
        CalendarEvent recurringEvent, DateTimeOffset rangeStart, DateTimeOffset rangeEnd)
    {
        var instances = new List<CalendarEvent>();
        var rrule = RecurrenceRule.Parse(recurringEvent.RRULE);

        // Generate all occurrences within the range
        var dates = GenerateOccurrences(rrule, recurringEvent.StartTime,
            rangeStart, rangeEnd);

        // Remove excluded dates (EXDATE)
        if (recurringEvent.RRULE.EXDATE != null)
        {
            dates = dates.Where(d => !recurringEvent.RRULE.EXDATE.Contains(
                d.ToString("yyyyMMdd"))).ToList();
        }

        foreach (var occurrenceDate in dates)
        {
            // Handle timezone and DST transitions
            var adjustedStart = AdjustForDST(occurrenceDate, recurringEvent);
            var duration = recurringEvent.EndTime - recurringEvent.StartTime;
            var adjustedEnd = adjustedStart + duration;

            // Check for modified instances (single occurrence changed)
            var modifiedInstance = FindModifiedInstance(
                recurringEvent.EventId, adjustedStart);

            if (modifiedInstance != null)
            {
                instances.Add(modifiedInstance); // Use the modified version
            }
            else
            {
                instances.Add(new CalendarEvent
                {
                    EventId = $"{recurringEvent.EventId}_{occurrenceDate:yyyyMMdd}",
                    RecurringEventId = recurringEvent.EventId,
                    Summary = recurringEvent.Summary,
                    Description = recurringEvent.Description,
                    StartTime = adjustedStart,
                    EndTime = adjustedEnd,
                    StartTimezone = recurringEvent.StartTimezone,
                    EndTimezone = recurringEvent.EndTimezone,
                    Status = recurringEvent.Status,
                    Attendees = recurringEvent.Attendees,
                    Reminders = recurringEvent.Reminders
                });
            }
        }

        return instances;
    }

    private DateTimeOffset AdjustForDST(DateTimeOffset date, CalendarEvent template)
    {
        // When a DST transition occurs during a recurring event,
        // the event should maintain its local time (wall clock time),
        // not its UTC offset.
        var tz = TimeZoneInfo.FindSystemTimeZoneById(template.StartTimezone);

        // Get the local time from the template
        var templateLocal = template.StartTime.LocalDateTime;

        // Apply the same local time to the new date
        var newLocal = date.LocalDateTime.Date + templateLocal.TimeOfDay;

        return new DateTimeOffset(newLocal, tz.GetUtcOffset(newLocal));
    }
}

Recurrence Edge Cases

Edge CaseExampleHandling
DST spring-forward2:30 AM doesn't existShift to 3:00 AM (or next valid time)
DST fall-back1:30 AM occurs twiceUse the first occurrence (before DST change)
Leap yearFeb 29 yearly recurrenceSkip non-leap years
Month-end31st of every monthClamp to last day of shorter months
Modified instanceChange one occurrence of weekly meetingStore as separate event with EXDATE on original
Deleted instanceCancel one occurrenceAdd EXDATE to RRULE, mark instance as cancelled

8. Timezone Handling

Multi-Timezone Architecture

Timezone Conversion Flow

sequenceDiagram participant U1 as User in New York participant S as Calendar Service participant TZ as Timezone Service participant U2 as User in London U1->>S: Create meeting at 2:00 PM EST S->>TZ: Convert 2:00 PM EST → UTC TZ-->>S: 19:00 UTC S->>S: Store as 19:00 UTC + timezone metadata U2->>S: View calendar S->>TZ: Convert 19:00 UTC → Europe/London TZ-->>S: 19:00 UTC = 7:00 PM GMT S-->>U2: Meeting shows as 7:00 PM
public class TimezoneService
{
    private readonly ConcurrentDictionary<string, TimeZoneInfo> _tzCache = new();

    public DateTimeOffset ConvertToUTC(DateTimeOffset localTime, string timezoneId)
    {
        var tz = GetTimezone(timezoneId);
        var utcOffset = tz.GetUtcOffset(localTime LocalDateTime);
        return localTime.ToOffset(utcOffset);
    }

    public DateTimeOffset ConvertFromUTC(DateTimeOffset utcTime, string targetTimezoneId)
    {
        var tz = GetTimezone(targetTimezoneId);
        return TimeZoneInfo.ConvertTime(utcTime, tz);
    }

    public string FormatForUser(DateTimeOffset utcTime, string userId)
    {
        var userTimezone = GetUserTimezone(userId);
        var localTime = ConvertFromUTC(utcTime, userTimezone);
        return localTime.ToString("ddd, MMM d, yyyy h:mm tt zzz");
    }

    private TimeZoneInfo GetTimezone(string timezoneId)
    {
        return _tzCache.GetOrAdd(timezoneId, id =>
        {
            try { return TimeZoneInfo.FindSystemTimeZoneById(id); }
            catch (TimeZoneNotFoundException)
            {
                // Fallback to UTC if timezone not found
                return TimeZoneInfo.Utc;
            }
        });
    }

    // Detect DST transitions for a given timezone
    public List<DSTTransition> GetDSTTransitions(string timezoneId, int year)
    {
        var tz = GetTimezone(timezoneId);
        var transitions = new List<DSTTransition>();

        var jan1 = new DateTime(year, 1, 1, 0, 0, 0);
        var dec31 = new DateTime(year, 12, 31, 23, 59, 59);

        for (var date = jan1; date <= dec31; date = date.AddDays(1))
        {
            var todayOffset = tz.GetUtcOffset(date);
            var tomorrowOffset = tz.GetUtcOffset(date.AddDays(1));

            if (todayOffset != tomorrowOffset)
            {
                transitions.Add(new DSTTransition
                {
                    Date = date.Date,
                    OffsetBefore = todayOffset,
                    OffsetAfter = tomorrowOffset,
                    IsSpringForward = todayOffset < tomorrowOffset
                });
            }
        }

        return transitions;
    }
}

9. Notification System

Notification Pipeline

graph TB subgraph "Trigger Sources" EC[Event Created] EU[Event Updated] ED[Event Deleted] RC[Reminder Check] end subgraph "Notification Service" EC --> NQ[Notification Queue] EU --> NQ ED --> NQ RC --> SCHED[Scheduler] SCHED --> NQ end subgraph "Dispatch" NQ --> ROUTER[Notification Router] ROUTER --> PUSH[Push Notifications] ROUTER --> EMAIL[Email] ROUTER --> SMS[SMS] ROUTER --> INAPP[In-App Alerts] end subgraph "Scheduling" SCHED --> CQ[Cassandra: Scheduled Notifications] CQ --> POLL[Poller: Check every minute] POLL --> NQ end
public class NotificationService
{
    private readonly INotificationQueue _queue;
    private readonly IScheduledNotificationStore _scheduledStore;
    private readonly IReminderScheduler _reminderScheduler;

    public async Task ScheduleEventRemindersAsync(CalendarEvent evt)
    {
        foreach (var reminder in evt.Reminders)
        {
            var notifyAt = evt.StartTime.AddMinutes(-reminder.MinutesBeforeEvent);

            if (notifyAt <= DateTimeOffset.UtcNow)
                continue; // Event already started, skip

            await _scheduledStore.SaveAsync(new ScheduledNotification
            {
                NotificationId = Guid.NewGuid().ToString("N"),
                EventId = evt.EventId,
                CalendarId = evt.CalendarId,
                UserId = GetNotificationRecipient(evt),
                Type = reminder.Method,
                ScheduledTime = notifyAt,
                Payload = new NotificationPayload
                {
                    Title = evt.Summary,
                    Body = $"Starting in {reminder.MinutesBeforeEvent} minutes",
                    Location = evt.Location,
                    EventStartTime = evt.StartTime,
                    DeepLink = $"calendar://event/{evt.EventId}"
                }
            });
        }
    }

    public async Task SendInvitationsAsync(CalendarEvent evt, List<EventAttendee> newAttendees)
    {
        foreach (var attendee in newAttendees.Where(a => !a.IsOrganizer))
        {
            await _queue.EnqueueAsync(new NotificationMessage
            {
                RecipientId = attendee.UserId,
                Type = NotificationType.Invitation,
                Priority = Priority.High,
                Payload = new InvitationPayload
                {
                    EventSummary = evt.Summary,
                    OrganizerName = evt.OrganizerId,
                    StartTime = evt.StartTime,
                    EndTime = evt.EndTime,
                    Location = evt.Location,
                    ActionUrl = $"calendar://invite/{evt.EventId}",
                    Actions = new[] { "Accept", "Tentative", "Decline" }
                }
            });
        }
    }
}

public class NotificationDispatcher
{
    private readonly IPushNotificationService _push;
    private readonly IEmailService _email;
    private readonly ISmsService _sms;

    public async Task DispatchAsync(NotificationMessage message)
    {
        var user = await _userService.GetAsync(message.RecipientId);
        var preferences = await GetNotificationPreferencesAsync(message.RecipientId);

        var tasks = new List<Task>();

        if (preferences.PushEnabled && message.Priority >= preferences.PushThreshold)
        {
            tasks.Add(_push.SendAsync(user.PushTokens, message));
        }

        if (preferences.EmailEnabled && message.Priority >= preferences.EmailThreshold)
        {
            tasks.Add(_email.SendAsync(user.Email, message));
        }

        if (preferences.SmsEnabled && message.Priority >= Priority.Critical)
        {
            tasks.Add(_sms.SendAsync(user.PhoneNumber, message));
        }

        await Task.WhenAll(tasks);
    }
}

10. Free/Busy Lookup

Precomputed Free/Busy Index

Free/busy queries must be extremely fast since they're called for every participant when scheduling a meeting. We precompute daily free/busy data and store it in an optimized format.

public class FreeBusyService
{
    private readonly IFreeBusyStore _store;
    private readonly IEventStore _eventStore;

    public async Task<FreeBusyResponse> GetFreeBusyAsync(
        List<string> userIds, DateTimeOffset start, DateTimeOffset end)
    {
        var response = new FreeBusyResponse();

        // Fetch precomputed free/busy data in parallel
        var tasks = userIds.Select(async userId =>
        {
            var busySlots = await _store.GetBusySlotsAsync(userId, start, end);
            return new UserFreeBusy
            {
                UserId = userId,
                BusySlots = busySlots
            };
        });

        response.Users = (await Task.WhenAll(tasks)).ToList();
        return response;
    }

    // Called by event service whenever an event is created/updated/deleted
    public async Task UpdateFreeBusyAsync(CalendarEvent evt, FreeBusyChange change)
    {
        var days = GetAffectedDays(evt.StartTime, evt.EndTime);

        foreach (var day in days)
        {
            await _store.UpdateDailyFreeBusyAsync(
                evt.Attendees.Select(a => a.UserId),
                day,
                new TimeSlot
                {
                    Start = evt.StartTime,
                    End = evt.EndTime,
                    EventId = evt.EventId,
                    Status = change == FreeBusyChange.Busy
                        ? SlotStatus.Busy
                        : SlotStatus.Free
                });
        }
    }
}

// Precomputed daily free/busy stored in a compact bitmap format
public class DailyFreeBusyBitmap
{
    // Represent a 24-hour day as 96 quarter-hour slots
    private const int SlotsPerDay = 96;
    private byte[] _busyBitmap = new byte[SlotsPerDay / 8]; // ~12 bytes per day

    public void MarkBusy(int startSlot, int endSlot)
    {
        for (int i = startSlot; i < endSlot; i++)
        {
            _busyBitmap[i / 8] |= (byte)(1 << (i % 8));
        }
    }

    public bool IsBusy(int slot)
    {
        return (_busyBitmap[slot / 8] & (1 << (slot % 8))) != 0;
    }

    public List<TimeSlot> GetBusySlots()
    {
        var slots = new List<TimeSlot>();
        bool inBusy = false;
        int start = 0;

        for (int i = 0; i < SlotsPerDay; i++)
        {
            if (IsBusy(i) && !inBusy)
            {
                start = i;
                inBusy = true;
            }
            else if (!IsBusy(i) && inBusy)
            {
                slots.Add(new TimeSlot
                {
                    Start = SlotToTime(start),
                    End = SlotToTime(i)
                });
                inBusy = false;
            }
        }

        if (inBusy) slots.Add(new TimeSlot { Start = SlotToTime(start), End = SlotToTime(SlotsPerDay) });
        return slots;
    }
}

11. Calendar Sharing and Permissions

Permission Model

Permission LevelCan ViewCan EditCan ShareSee Free/Busy
OwnerFullFullFullYes
WriterFull detailsCreate/Edit/DeleteNoYes
ReaderFull detailsNoNoYes
Free/Busy onlyBusy/Free onlyNoNoYes
public class CalendarAccessControl
{
    private readonly ICalendarACLStore _aclStore;

    public async Task<bool> HasPermissionAsync(string userId, string calendarId,
        CalendarPermission requiredPermission)
    {
        // Check direct ownership
        var calendar = await _calendarStore.GetAsync(calendarId);
        if (calendar.OwnerUserId == userId)
            return true;

        // Check ACL entries
        var acl = await _aclStore.GetAsync(calendarId, userId);
        if (acl == null)
            return false; // No access

        return acl.Role >= requiredPermission;
    }

    public async Task ShareCalendarAsync(string calendarId, string granteeEmail,
        CalendarRole role, string sharerUserId)
    {
        // Verify sharer has share permission
        if (!await HasPermissionAsync(sharerUserId, calendarId, CalendarPermission.Share))
            throw new UnauthorizedAccessException("You don't have permission to share this calendar");

        var grantee = await _userService.GetByEmailAsync(granteeEmail);
        if (grantee == null)
            throw new UserNotFoundException($"No user found with email {granteeEmail}");

        await _aclStore.SaveAsync(new CalendarACL
        {
            CalendarId = calendarId,
            GranteeId = grantee.UserId,
            GranteeType = GranteeType.User,
            Role = role,
            Scope = ACLScope.Default
        });

        // Send sharing notification
        await _notificationService.SendAsync(new CalendarSharedNotification
        {
            CalendarId = calendarId,
            CalendarTitle = (await _calendarStore.GetAsync(calendarId)).Title,
            SharedByUserId = sharerUserId,
            GrantedRole = role,
            GranteeUserId = grantee.UserId
        });
    }
}

12. Multi-Device Sync Protocol

Sync Token Architecture

Incremental Sync Flow

sequenceDiagram participant C as Client Device participant S as Sync Service participant DB as Database Note over C,S: First sync (full sync) C->>S: GET /sync?token=0 S->>DB: Get all events for user DB-->>S: All events + sync_token=1001 S-->>C: Full event list + token=1001 Note over C,S: Subsequent syncs (incremental) C->>S: GET /sync?token=1001 S->>DB: Get changes since token=1001 DB-->>S: Modified/deleted events + token=1005 S-->>C: Delta changes + token=1005 Note over C,S: Conflict resolution C->>S: PUT /events/{id} (ETag: 1003) S->>DB: Check ETag alt ETag matches DB-->>S: Updated successfully S-->>C: Success + new ETag=1006 else ETag mismatch DB-->>S: Conflict detected S-->>C: 409 Conflict + latest version end
public class SyncService
{
    private readonly ISyncTokenStore _tokenStore;
    private readonly IEventStore _eventStore;
    private readonly IChangeLog _changeLog;

    public async Task<SyncResponse> SyncAsync(string userId, long? lastSyncToken)
    {
        if (lastSyncToken == null || lastSyncToken == 0)
        {
            // Full sync
            return await FullSyncAsync(userId);
        }

        // Incremental sync
        var changes = await _changeLog.GetChangesAsync(userId, lastSyncToken.Value);
        var newToken = await _tokenStore.GetLatestTokenAsync(userId);

        var response = new SyncResponse
        {
            SyncToken = newToken,
            Events = new List<SyncEvent>()
        };

        foreach (var change in changes)
        {
            switch (change.ChangeType)
            {
                case ChangeType.Created:
                case ChangeType.Updated:
                    var evt = await _eventStore.GetAsync(change.EventId);
                    if (evt != null)
                    {
                        response.Events.Add(new SyncEvent
                        {
                            Event = evt,
                            ChangeType = change.ChangeType
                        });
                    }
                    break;
                case ChangeType.Deleted:
                    response.Events.Add(new SyncEvent
                    {
                        EventId = change.EventId,
                        ChangeType = ChangeType.Deleted
                    });
                    break;
            }
        }

        return response;
    }

    private async Task<SyncResponse> FullSyncAsync(string userId)
    {
        var events = await _eventStore.GetAllEventsAsync(userId);
        var token = await _tokenStore.GetLatestTokenAsync(userId);

        return new SyncResponse
        {
            SyncToken = token,
            Events = events.Select(e => new SyncEvent
            {
                Event = e,
                ChangeType = ChangeType.Created
            }).ToList()
        };
    }
}

13. Read/Write Paths

Write Path: Creating an Event

Event Creation Flow

sequenceDiagram participant C as Client participant GW as API Gateway participant ES as Event Service participant CD as Conflict Detector participant DB as Event Database participant NS as Notification Service participant CS as Calendar Sync C->>GW: POST /events {summary, time, attendees} GW->>ES: Create event request ES->>CD: Check conflicts for calendar CD->>DB: Query events in time range DB-->>CD: Existing events alt No conflicts CD-->>ES: No conflicts ES->>DB: Save event (atomic) DB-->>ES: Saved with ETag=1 ES->>NS: Schedule reminders + send invitations ES->>CS: Broadcast change to all devices ES-->>C: 201 Created + event details else Conflicts found CD-->>ES: Conflict list ES-->>C: 409 Conflict + suggestions end

Read Path: Loading Calendar View

public class CalendarViewService
{
    private readonly IEventStore _eventStore;
    private readonly ICacheService _cache;
    private readonly IFreeBusyService _freeBusy;
    private readonly IRecurrenceExpander _recurrence;

    public async Task<CalendarView> GetViewAsync(string userId, DateTimeOffset start,
        DateTimeOffset end, List<string> calendarIds = null)
    {
        // Get user's calendars if not specified
        calendarIds ??= await GetUserCalendarIdsAsync(userId);

        var cacheKey = $"cal-view:{userId}:{start:yyyyMMdd}:{end:yyyyMMdd}";
        var cached = await _cache.GetAsync<CalendarView>(cacheKey);
        if (cached != null) return cached;

        // Fetch events from all calendars in parallel
        var allEvents = new List<CalendarEvent>();
        var tasks = calendarIds.Select(async calId =>
        {
            var events = await _eventStore.GetEventsInRangeAsync(calId, start, end);
            return events;
        });

        var results = await Task.WhenAll(tasks);
        foreach (var calEvents in results)
            allEvents.AddRange(calEvents);

        // Expand recurring events
        var expandedEvents = new List<CalendarEvent>();
        foreach (var evt in allEvents)
        {
            if (evt.RRULE != null)
            {
                expandedEvents.AddRange(_recurrence.ExpandRecurrence(evt, start, end));
            }
            else
            {
                expandedEvents.Add(evt);
            }
        }

        // Sort by start time
        expandedEvents = expandedEvents.OrderBy(e => e.StartTime).ToList();

        var view = new CalendarView
        {
            UserId = userId,
            Start = start,
            End = end,
            Events = expandedEvents,
            DayGroups = GroupByDay(expandedEvents, start, end)
        };

        await _cache.SetAsync(cacheKey, view, TimeSpan.FromMinutes(5));
        return view;
    }
}

14. Failure Scenarios

FailureImpactDetectionMitigationRecovery
Event DB downCannot create/view eventsHealth check timeoutRead from replica; queue writesDB failover to replica
Notification service downReminders not deliveredQueue depth growingRetry with exponential backoffService restart + catch-up
Cache failureHigher DB loadCache miss rate spikeDirect DB reads, rate limitRedis cluster recovery
Timezone data staleWrong event timesTest with known DST datesUpdate IANA TZ databaseAutomatic update pipeline
Sync token corruptedClient desyncClient reports stale dataForce full resyncClient re-syncs from scratch
Recurrence expansion timeoutComplex recurring events slowRequest latency > 5sCache expanded instancesPre-expand common patterns

15. Cost Estimation

ComponentSpecMonthly Cost
Event Servicec5.xlarge × 20~$5K
Notification Servicem5.xlarge × 10~$2.5K
Sync Servicec5.xlarge × 15~$4K
Cassandra clusteri3.2xlarge × 30~$18K
Redis clusterr5.xlarge × 12~$6K
S3 (attachments)~500TB~$12K
SQS (notification queue)~2B msgs/day~$1.5K
Email (SES)~500M emails/day~$50K
Push notifications (FCM/APNs)~1B/day~$2K
Total~$101K/month

16. Interview Questions and Answers

Q1: How do you handle double-booking across shared calendars?

Use a distributed lock on the calendar ID during event creation, combined with a database-level unique constraint on (calendar_id, start_time, end_time). The lock prevents concurrent modifications, and the unique constraint provides a safety net. For cross-calendar conflict detection (checking all attendees' calendars), use a two-phase approach: first check conflicts optimistically, then acquire locks only for calendars with detected conflicts before finalizing.

Q2: How would you handle recurring events with DST transitions?

Store recurring events with their local timezone (wall clock time), not UTC. When expanding instances, apply the local time to each occurrence date and let the timezone database resolve the UTC offset. For spring-forward (2:30 AM doesn't exist), shift to the next valid time. For fall-back (1:30 AM occurs twice), use the first occurrence. The IANA timezone database contains all historical DST rules.

Q3: How do you handle calendar sharing permissions at scale?

Use a hierarchical permission model: Owner → Writer → Reader → Free/Busy. Store ACLs in a separate table indexed by (calendar_id, grantee_id) for O(1) permission checks. Cache ACL results in Redis with a 5-minute TTL. For organizations, support group-based permissions (e.g., "all employees in engineering" can view the team calendar). Permission checks happen at the API gateway level before requests reach the event service.

Q4: How do you sync calendars across multiple devices?

Use a sync token protocol similar to Google's. Each user has a monotonically increasing sync token. Clients store their last sync token and request only changes since that token. The server maintains a change log that records every event modification. When a client syncs, it receives only the delta (created, updated, deleted events). For real-time updates, use long-polling or server-sent events (SSE) to push changes to connected clients.

Q5: How do you handle free/busy lookups for users with millions of events?

Precompute daily free/busy bitmaps for each user. Each day is represented as 96 quarter-hour slots (12 bytes per day). Free/busy queries become simple bitmap lookups — O(1) per day. When events are created or modified, update the affected daily bitmaps asynchronously. For users with dense calendars, this precomputation avoids scanning all events for every free/busy query.

Q6: How would you handle a scenario where the notification service is down?

Notifications are stored in a durable queue (SQS/SNS) before being dispatched. If the dispatch service is down, notifications remain in the queue and are retried when the service recovers. For time-critical notifications (meeting reminders), implement a priority queue that processes reminders with the most urgent send time first. Add a "notification lag" metric and alert if the lag exceeds 60 seconds.

Q7: How do you handle offline edits on mobile and sync when back online?

Use an offline-first architecture with a local SQLite database on the mobile device. All edits are applied locally first (immediate UI response). When connectivity is restored, the client sends changes to the server with the last known sync token. The server detects conflicts using optimistic concurrency (ETags). If a conflict is detected, the client is notified and the user chooses which version to keep. This ensures no data loss while maintaining consistency.