Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/0f08b6dc7bba844504f635823e792f61 to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/0f08b6dc7bba844504f635823e792f61 to your computer and use it in GitHub Desktop.

Revisions

  1. carefree-ladka created this gist May 7, 2026.
    1,066 changes: 1,066 additions & 0 deletions Google Calendar — Frontend System Design.mdx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1066 @@
    # Google Calendar — Frontend System Design
    ### A Complete Guide to Cracking FAANG/MAANG Interviews

    ---

    ## Table of Contents

    1. [Problem Statement & Scope](#1-problem-statement--scope)
    2. [Functional Requirements](#2-functional-requirements)
    3. [Non-Functional Requirements](#3-non-functional-requirements)
    4. [Data Modelling](#4-data-modelling)
    - 4.1 [Event Schema](#41-event-schema)
    - 4.2 [Recurrence Rule (RRULE) Model](#42-recurrence-rule-rrule-model)
    - 4.3 [User & Calendar Schema](#43-user--calendar-schema)
    - 4.4 [Notification Schema](#44-notification-schema)
    5. [High-Level Architecture](#5-high-level-architecture)
    - 5.1 [Component Tree](#51-component-tree)
    - 5.2 [State Management Architecture](#52-state-management-architecture)
    - 5.3 [Data Flow](#53-data-flow)
    6. [UI Views & Rendering](#6-ui-views--rendering)
    - 6.1 [Month View](#61-month-view)
    - 6.2 [Week View](#62-week-view)
    - 6.3 [Day View](#63-day-view)
    - 6.4 [Agenda View](#64-agenda-view)
    7. [Recurrence Engine](#7-recurrence-engine)
    - 7.1 [RRULE Standard](#71-rrule-standard)
    - 7.2 [Expansion Algorithm](#72-expansion-algorithm)
    - 7.3 [Edit Modes: This / This & Following / All](#73-edit-modes-this--this--following--all)
    8. [Drag & Drop System](#8-drag--drop-system)
    9. [Timezone Handling](#9-timezone-handling)
    10. [Performance Optimisations](#10-performance-optimisations)
    - 10.1 [Virtual Rendering](#101-virtual-rendering)
    - 10.2 [Event Overlap Layout Algorithm](#102-event-overlap-layout-algorithm)
    - 10.3 [Caching & Prefetching](#103-caching--prefetching)
    11. [Real-Time Sync & Conflict Resolution](#11-real-time-sync--conflict-resolution)
    12. [Offline Support & Service Workers](#12-offline-support--service-workers)
    13. [Accessibility (a11y)](#13-accessibility-a11y)
    14. [API Contract](#14-api-contract)
    15. [Testing Strategy](#15-testing-strategy)
    16. [Interview Cheat Sheet](#16-interview-cheat-sheet)

    ---

    ## 1. Problem Statement & Scope

    Design the **frontend** of a Google Calendar-like application that supports:

    - Creating, editing, and deleting events
    - Multiple calendar views (month, week, day, agenda)
    - Recurring events with full RFC 5545 iCalendar RRULE support
    - Multi-timezone display
    - Drag-and-drop scheduling
    - Real-time collaboration (shared calendars)
    - Offline-first capability

    **Out of scope for this discussion:** Backend service design, authentication infrastructure, billing systems.

    **Scale assumptions:**
    - A power user has ~500 events/month across 5 calendars
    - View renders in < 100ms after data is cached
    - Real-time updates propagate in < 2 seconds

    ---

    ## 2. Functional Requirements

    ### Core (P0)
    - **CRUD events** — title, start/end datetime, location, description, guests, colour
    - **Recurring events** — daily, weekly, monthly, yearly, with exceptions
    - **Calendar views** — month, week, 3-day, day, schedule/agenda
    - **Multi-calendar** — personal, shared, subscribed (read-only), birthdays
    - **Invitations** — accept / decline / tentative with RSVP tracking
    - **Notifications** — browser push, in-app badges, email digest
    - **Timezone** — per-user setting; display events in local time; all-day events span midnight correctly

    ### Extended (P1)
    - **Drag & drop** — move event across time slots; resize event duration
    - **Quick-add** — natural language input ("Lunch with Tom Friday 1pm")
    - **Event search** — full-text across title, description, guests
    - **Conflict detection** — warn when two events overlap
    - **Goals & tasks** — separate entity type with auto-scheduling AI
    - **Google Meet** integration — auto-generate video link

    ### Nice-to-have (P2)
    - **Dark mode**
    - **Keyboard shortcuts** (n = new event, t = today, 1/2/3 = day/week/month)
    - **Print view**
    - **ICS import/export**

    ---

    ## 3. Non-Functional Requirements

    | Quality Attribute | Target | How Achieved |
    |---|---|---|
    | **Initial Load (TTI)** | < 3s on 4G | Code splitting per view, skeleton screens |
    | **View Switch** | < 100ms | Pre-rendered adjacent views, virtual scroll |
    | **Event Render (week, 200 events)** | < 16ms (60fps) | Canvas fallback, CSS containment |
    | **Availability** | 99.9% | Service Worker + IndexedDB offline cache |
    | **Real-time latency** | < 2s | WebSocket / Server-Sent Events (SSE) |
    | **Accessibility** | WCAG 2.1 AA | ARIA grid roles, keyboard nav, focus traps |
    | **Bundle size** | < 150KB gzipped (initial) | Tree shaking, lazy routes |
    | **Memory** | < 100MB for 12-month view | Event virtualisation, WeakMap caches |

    ---

    ## 4. Data Modelling

    ### 4.1 Event Schema

    The canonical event object lives in the frontend store. It mirrors the Google Calendar API v3 Event resource with some client-side additions.

    ```typescript
    interface CalendarEvent {
    // Identity
    id: string; // UUID, local-first generated
    calendarId: string;
    etag: string; // Optimistic concurrency token

    // What
    title: string;
    description?: string;
    location?: string;
    colorId?: CalendarColor; // Enum: 'tomato' | 'flamingo' | 'tangerine' | ...
    attachments?: Attachment[];
    conferenceData?: ConferenceData; // Google Meet link

    // When
    start: EventDateTime; // { dateTime: ISO8601, timeZone } OR { date: 'YYYY-MM-DD' }
    end: EventDateTime;
    allDay: boolean;

    // Recurrence
    recurrence?: string[]; // RRULE, EXRULE, RDATE, EXDATE strings
    recurringEventId?: string; // Points to the master event
    originalStartTime?: EventDateTime; // For exceptions/instances
    isException: boolean; // This instance deviates from RRULE

    // Who
    organizer: Person;
    creator: Person;
    attendees?: Attendee[];
    guestsCanModify: boolean;
    guestsCanInviteOthers: boolean;

    // State
    status: 'confirmed' | 'tentative' | 'cancelled';
    visibility: 'default' | 'public' | 'private' | 'confidential';
    transparency: 'opaque' | 'transparent'; // Blocks time or not

    // Notifications
    reminders: {
    useDefault: boolean;
    overrides?: Reminder[];
    };

    // System
    created: ISO8601;
    updated: ISO8601;
    sequence: number; // iCal SEQUENCE for change tracking
    syncToken?: string;

    // Client-only (not persisted to server)
    _localStatus?: 'syncing' | 'error' | 'dirty';
    _computedInstances?: EventInstance[]; // Expanded recurrence instances
    }

    interface EventDateTime {
    date?: string; // 'YYYY-MM-DD' for all-day events
    dateTime?: string; // ISO8601 with timezone for timed events
    timeZone?: string; // IANA tz identifier e.g. 'America/New_York'
    }

    interface Attendee {
    email: string;
    displayName?: string;
    self?: boolean;
    responseStatus: 'needsAction' | 'declined' | 'tentative' | 'accepted';
    organizer?: boolean;
    resource?: boolean; // Conference room
    }

    interface Reminder {
    method: 'email' | 'popup';
    minutes: number;
    }
    ```

    ### 4.2 Recurrence Rule (RRULE) Model

    Recurrence is stored as RFC 5545 strings (the same format as iCal). The frontend ships a lightweight RRULE expansion engine.

    ```typescript
    // Parsed RRULE object (internal to the recurrence engine)
    interface ParsedRRule {
    freq: 'SECONDLY' | 'MINUTELY' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
    interval?: number; // Default 1. FREQ=WEEKLY;INTERVAL=2 = biweekly
    until?: Date; // Recur until this datetime (exclusive)
    count?: number; // Recur exactly N times (mutually exclusive with until)
    byDay?: ByDay[]; // e.g. ['MO', 'WE', 'FR'] or ['-1SA'] (last Saturday)
    byMonthDay?: number[]; // e.g. [1, 15] = 1st and 15th of month
    byMonth?: number[]; // e.g. [1, 7] = January and July
    bySetPos?: number[]; // e.g. [-1] = last occurrence in set
    weekStart?: DayOfWeek; // Default MO
    exDates?: Date[]; // Dates excluded from rule (EXDATE)
    rDates?: Date[]; // Extra dates added outside rule (RDATE)
    }

    type ByDay = DayOfWeek | `${'-' | ''}${number}${DayOfWeek}`;
    type DayOfWeek = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
    ```

    **Common RRULE examples:**

    ```
    # Every weekday
    RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
    # Every 2 weeks on Monday
    RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO
    # Last Friday of every month
    RRULE:FREQ=MONTHLY;BYDAY=-1FR
    # First Monday of every month
    RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1
    # Every year on March 15, ending after 5 occurrences
    RRULE:FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15;COUNT=5
    ```

    ### 4.3 User & Calendar Schema

    ```typescript
    interface UserSettings {
    userId: string;
    displayName: string;
    email: string;
    photoUrl?: string;
    locale: string; // 'en-US'
    timezone: string; // 'Asia/Kolkata'
    weekStart: 0 | 1 | 6; // 0=Sun, 1=Mon, 6=Sat
    use24HourTime: boolean;
    defaultView: ViewType;
    defaultReminderMinutes: number;
    workingHours: { start: string; end: string; days: number[] };
    }

    interface Calendar {
    id: string;
    summary: string;
    description?: string;
    timeZone: string;
    colorId: CalendarColor;
    backgroundColor: string; // Hex
    foregroundColor: string; // Hex for text on background
    accessRole: 'freeBusyReader' | 'reader' | 'writer' | 'owner';
    primary?: boolean; // User's main calendar
    selected: boolean; // Shown in UI
    kind: 'user' | 'shared' | 'resource' | 'birthday' | 'holiday';
    }
    ```

    ### 4.4 Notification Schema

    ```typescript
    interface Notification {
    id: string;
    eventId: string;
    calendarId: string;
    eventTitle: string;
    eventStart: string;
    triggerTime: string; // When to fire the notification
    method: 'popup' | 'email';
    dismissed: boolean;
    snoozedUntil?: string;
    }
    ```

    ---

    ## 5. High-Level Architecture

    ### 5.1 Component Tree

    ```
    App
    ├── RouterProvider
    │ ├── AuthGuard
    │ └── CalendarShell
    │ ├── TopBar
    │ │ ├── Logo
    │ │ ├── DateNavigator ← prev / today / next
    │ │ ├── ViewSwitcher ← Day | Week | Month | Agenda
    │ │ └── SearchBar
    │ ├── SidePanel
    │ │ ├── MiniCalendar ← month thumbnail, clickable
    │ │ ├── CalendarList ← toggleable calendar checkboxes
    │ │ └── CreateEventButton
    │ └── MainView ← code-split per view
    │ ├── MonthView
    │ │ └── MonthGrid → WeekRow[] → DayCell[] → EventChip[]
    │ ├── WeekView
    │ │ ├── AllDayBanner ← multi-day / all-day events
    │ │ └── TimeGrid ← hourly rows × 7 columns
    │ │ └── TimeSlot[] → EventBlock[]
    │ ├── DayView ← same as WeekView, single column
    │ └── AgendaView
    │ └── AgendaGroup[] → AgendaItem[]
    ├── EventModal ← shared overlay for create/edit
    │ ├── EventForm
    │ ├── RecurrenceEditor
    │ ├── GuestPicker
    │ ├── ReminderEditor
    │ └── ConferenceToggle
    └── NotificationCenter
    ```

    ### 5.2 State Management Architecture

    Use a **layered state** approach:

    ```
    ┌─────────────────────────────────────────────────────────┐
    │ Server State (React Query / SWR) │
    │ - events per calendar per date range │
    │ - user settings │
    │ - calendar list │
    │ - Stale-while-revalidate + background sync │
    └───────────────────────┬─────────────────────────────────┘
    │ normalized + merged into
    ┌───────────────────────▼─────────────────────────────────┐
    │ Global UI State (Zustand / Redux Toolkit) │
    │ - currentView: ViewType │
    │ - selectedDate: Date │
    │ - visibleDateRange: { start, end } │
    │ - activeCalendars: Set<calendarId> │
    │ - draggingEventId: string | null │
    │ - openModal: ModalState | null │
    └───────────────────────┬─────────────────────────────────┘
    │ derived
    ┌───────────────────────▼─────────────────────────────────┐
    │ Computed / Derived State (useMemo / Reselect) │
    │ - expandedRecurrences: EventInstance[] │
    │ - overlapGroups: OverlapGroup[] │
    │ - conflictMap: Map<eventId, eventId[]> │
    └─────────────────────────────────────────────────────────┘
    │ local component state
    ┌───────────────────────▼─────────────────────────────────┐
    │ Local / Component State (useState / useReducer) │
    │ - form field values │
    │ - hover state │
    │ - tooltip visibility │
    └─────────────────────────────────────────────────────────┘
    ```

    ### 5.3 Data Flow

    ```
    User Action (click/drag)
    Component Handler
    ├── Optimistic UI update (local state mutated immediately)
    ├── API mutation dispatched (React Query mutate)
    ├── Server responds:
    │ success → merge server response, clear dirty flag
    │ failure → rollback optimistic update, show toast
    └── WebSocket event arrives from other collaborator
    → merge into normalized cache
    → re-render affected view slices
    ```

    ---

    ## 6. UI Views & Rendering

    ### 6.1 Month View

    **Layout:** A 6-row × 7-column CSS grid. Each cell is a `DayCell`.

    ```
    [Mon] [Tue] [Wed] [Thu] [Fri] [Sat] [Sun]
    [ 27] 28 29 30 31 [ 1] [ 2] ← greyed = outside current month
    [ 3] 4 5 6 7 8 9
    ...
    ```

    **Challenges & solutions:**

    - **Multi-day events** span across cells. Solved with absolute positioning relative to the week row container, not individual cells. An event from Wed–Fri gets `left: 2/7 * 100%` and `width: 3/7 * 100%`.
    - **"More" overflow** — a cell with > 3 events shows "+2 more" which opens a popover. Calculate visible event slots by measuring cell height at render time.
    - **Memoisation** — each `WeekRow` is wrapped in `React.memo`. Rows only re-render if their set of events changes (shallow compare on event ID arrays).

    ### 6.2 Week View

    **Layout:** Two zones stacked vertically:

    1. **All-day banner** — a separate scrollable row for events spanning > 24h or marked `allDay: true`
    2. **Time grid** — a scrollable 24-hour × 7-column grid

    ```
    Mon 12 Tue 13 Wed 14 ...
    all-day [Team meeting──────────]
    8:00 [Standup]
    8:30
    9:00 [Design review]
    ...
    ```

    **Pixel-to-time mapping:**

    ```typescript
    const HOUR_HEIGHT = 60; // px per hour at 100% zoom

    function timeToY(datetime: Date, dayStart: Date): number {
    const minutesFromDayStart = differenceInMinutes(datetime, dayStart);
    return (minutesFromDayStart / 60) * HOUR_HEIGHT;
    }

    function yToTime(y: number, dayStart: Date): Date {
    const minutes = Math.round((y / HOUR_HEIGHT) * 60);
    return addMinutes(dayStart, minutes);
    }
    ```

    **Zoom levels:** 100% (60px/hr), 150% (90px/hr), 200% (120px/hr). Zoom state is persisted in `localStorage`.

    ### 6.3 Day View

    Identical to Week View but with a single column. The time grid takes full width, allowing longer event titles to show. The all-day banner handles the same logic.

    ### 6.4 Agenda View

    A chronological flat list grouped by date. Infinite scroll loads future dates on demand (intersection observer on the sentinel element).

    ```typescript
    interface AgendaGroup {
    date: Date; // Group header e.g. "Monday, June 12"
    events: EventInstance[];
    }

    // Render pattern
    <AgendaContainer>
    {groups.map(group => (
    <AgendaGroup key={group.date.toISOString()} date={group.date}>
    {group.events.map(event => <AgendaItem key={event.instanceId} event={event} />)}
    </AgendaGroup>
    ))}
    <Sentinel ref={sentinelRef} /> {/* triggers load more */}
    </AgendaContainer>
    ```

    ---

    ## 7. Recurrence Engine

    ### 7.1 RRULE Standard

    The RFC 5545 iCalendar spec defines recurrence rules. Key properties:

    | Property | Meaning | Example |
    |---|---|---|
    | `FREQ` | Frequency | `WEEKLY` |
    | `INTERVAL` | Every N units of FREQ | `INTERVAL=2` (biweekly) |
    | `BYDAY` | Day filter | `BYDAY=MO,FR` |
    | `BYMONTHDAY` | Day-of-month | `BYMONTHDAY=1,-1` (first & last) |
    | `UNTIL` | End date | `UNTIL=20261231T235959Z` |
    | `COUNT` | Max occurrences | `COUNT=10` |
    | `EXDATE` | Exception dates | Specific dates to skip |
    | `RDATE` | Extra dates | Additional occurrences added outside rule |

    ### 7.2 Expansion Algorithm

    **The key insight:** The server returns only master events with RRULE strings. The frontend expands them into concrete instances for the visible date range. This is called "client-side expansion."

    ```typescript
    function expandRecurrence(
    master: CalendarEvent,
    viewStart: Date,
    viewEnd: Date,
    maxInstances = 300
    ): EventInstance[] {
    const rule = parseRRule(master.recurrence!);
    const instances: EventInstance[] = [];
    const duration = differenceInMilliseconds(
    parseISO(master.end.dateTime!),
    parseISO(master.start.dateTime!)
    );

    let cursor = parseISO(master.start.dateTime!);
    let count = 0;

    while (cursor <= viewEnd && count < maxInstances) {
    if (cursor >= viewStart && !isExcluded(cursor, rule.exDates)) {
    instances.push(makeInstance(master, cursor, addMilliseconds(cursor, duration)));
    }
    cursor = nextOccurrence(cursor, rule); // Core: advance by FREQ × INTERVAL
    if (rule.until && cursor > rule.until) break;
    if (rule.count && count >= rule.count) break;
    count++;
    }

    // Merge server-confirmed exceptions (edits to individual instances)
    return mergeExceptions(instances, master.id);
    }
    ```

    **nextOccurrence implementation sketch:**

    ```typescript
    function nextOccurrence(current: Date, rule: ParsedRRule): Date {
    switch (rule.freq) {
    case 'DAILY': return addDays(current, rule.interval ?? 1);
    case 'WEEKLY': return nextWeeklyOccurrence(current, rule);
    case 'MONTHLY': return nextMonthlyOccurrence(current, rule);
    case 'YEARLY': return addYears(current, rule.interval ?? 1);
    // ... etc
    }
    }

    function nextWeeklyOccurrence(current: Date, rule: ParsedRRule): Date {
    const days = rule.byDay ?? [toDayCode(current)];
    // Find next matching day within the week, then skip by interval weeks
    // ... implementation with getDay() comparisons
    }
    ```

    **Why client-side expansion?**
    - Reduces API chattiness — one request per calendar fetch, not per rendered week
    - Enables instant navigation (no network round trip for prev/next week)
    - Allows snappy drag-and-drop preview before committing

    **Caching expanded instances:**

    ```typescript
    // Memoised with the master event ID + RRULE string + date range as cache key
    const expansionCache = new LRUCache<string, EventInstance[]>({ max: 200 });

    const cacheKey = `${master.id}:${master.recurrence?.join('|')}:${viewStart.toISOString()}:${viewEnd.toISOString()}`;
    ```

    ### 7.3 Edit Modes: This / This & Following / All

    When a user edits a recurring event instance, they get three choices:

    **1. This event only (exception)**
    - Create a new exception event with `recurringEventId = masterId` and `originalStartTime = instance.start`
    - The RRULE remains unchanged; EXDATE for the original slot is added if needed (when moving, not just editing)

    ```
    Master: RRULE weekly on Mon
    Instances: Mon1 Mon2 Mon3 [Mon4→Tue4 exception] Mon5
    ```

    **2. This and following events**
    - Truncate master's UNTIL to the day before the selected instance
    - Create a new master event starting from the selected instance with the same RRULE, optionally modified

    ```
    Master A: RRULE weekly on Mon, UNTIL=Mon3
    New Master B: RRULE weekly on Mon, starting Mon4 (with edits applied)
    ```

    **3. All events**
    - Update the master event directly
    - All exceptions that no longer deviate from the new master may be cleaned up
    - Show conflict warning if this overwrites user-specific exceptions

    ---

    ## 8. Drag & Drop System

    **Library choice:** `@dnd-kit/core` (lighter than react-beautiful-dnd, touch-friendly, supports accessibility).

    **Architecture:**

    ```typescript
    // DndContext wraps the entire TimeGrid
    <DndContext
    sensors={sensors} // PointerSensor + KeyboardSensor
    collisionDetection={closestCorner}
    onDragStart={handleDragStart}
    onDragOver={handleDragOver} // Preview snapping
    onDragEnd={handleDragEnd} // Commit or rollback
    >
    <TimeGrid>
    {events.map(event => (
    <Draggable key={event.instanceId} id={event.instanceId}>
    <EventBlock event={event} />
    </Draggable>
    ))}
    </TimeGrid>
    <DragOverlay> {/* Renders drag ghost */}
    {draggingEvent && <EventBlock event={draggingEvent} isGhost />}
    </DragOverlay>
    </DndContext>
    ```

    **Snapping logic:**

    ```typescript
    function snapToSlot(y: number, snapMinutes = 15): Date {
    const raw = yToTime(y, dayStart);
    const mins = getMinutes(raw) + getHours(raw) * 60;
    const snapped = Math.round(mins / snapMinutes) * snapMinutes;
    return setHours(setMinutes(dayStart, snapped % 60), Math.floor(snapped / 60));
    }
    ```

    **Recurrence drag:** When dragging an instance of a recurring event, immediately prompt the user with the edit-mode dialog (This / This & Following / All). Do not commit the drag until the user confirms.

    **Resize handle:** The bottom of each `EventBlock` has a 6px drag handle. Dragging it adjusts `event.end` while keeping `event.start` fixed. Minimum event duration: 15 minutes.

    ---

    ## 9. Timezone Handling

    Timezone handling is among the most subtle parts of a calendar frontend. Getting it wrong causes incorrect event display across DST boundaries or for guests in other zones.

    **Rules:**

    1. **All-day events are timezone-agnostic.** They are stored as `date: 'YYYY-MM-DD'` without time or zone. They always render as the full day in the user's local view, regardless of what timezone they're in.

    2. **Timed events are stored in UTC.** Display converts to the user's preferred timezone using the IANA database.

    3. **DST (Daylight Saving Time) transitions:** When an event that recurs weekly crosses a DST boundary, future instances should keep the same "clock time" (e.g. 9:00 AM), not the same UTC offset. This requires computing each instance's local time individually, not by adding fixed offsets.

    ```typescript
    // WRONG: adding fixed offset across DST
    const nextInstance = addMilliseconds(instance, 7 * 24 * 60 * 60 * 1000);

    // CORRECT: use date-fns-tz to work in the target timezone
    import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';

    function nextWeekSameLocalTime(instance: Date, tz: string): Date {
    const localTime = utcToZonedTime(instance, tz);
    const nextLocal = addWeeks(localTime, 1);
    return zonedTimeToUtc(nextLocal, tz);
    }
    ```

    4. **Guest timezone display:** On event detail view, show the event time in both the organizer's timezone and the viewer's timezone if they differ.

    5. **"Floating" times:** All-day events spanning multiple days (multi-day events entered without times) should not have their end date shifted by UTC conversion. Store as `date` not `dateTime`.

    **Library:** `date-fns-tz` (tree-shakeable, 40KB) or `Temporal` polyfill (future-proof, better API).

    ---

    ## 10. Performance Optimisations

    ### 10.1 Virtual Rendering

    The agenda view uses virtual scrolling to handle thousands of events without rendering them all:

    ```typescript
    import { useVirtualizer } from '@tanstack/react-virtual';

    const virtualizer = useVirtualizer({
    count: flattenedItems.length,
    getScrollElement: () => scrollRef.current,
    estimateSize: (i) => flattenedItems[i].type === 'header' ? 40 : 64,
    overscan: 10,
    });
    ```

    Month view cells that are out of the visible viewport use `content-visibility: auto` CSS:

    ```css
    .month-row {
    content-visibility: auto;
    contain-intrinsic-size: 0 120px; /* estimated height */
    }
    ```

    ### 10.2 Event Overlap Layout Algorithm

    When multiple events overlap in the time grid, they must be laid out side-by-side. This is a classic interval scheduling / graph colouring problem.

    **Algorithm (O(n log n) sweep line):**

    ```typescript
    interface LayoutColumn {
    events: EventInstance[];
    end: Date;
    }

    function computeOverlapLayout(events: EventInstance[]): Map<string, Layout> {
    // 1. Sort events by start time, then by duration descending
    const sorted = [...events].sort((a, b) =>
    a.start - b.start || (b.end - b.start) - (a.end - a.start)
    );

    const columns: LayoutColumn[] = [];
    const layoutMap = new Map<string, Layout>();

    for (const event of sorted) {
    // 2. Find the first column where the last event ends before this event starts
    let placed = false;
    for (let col = 0; col < columns.length; col++) {
    if (columns[col].end <= event.start) {
    columns[col].events.push(event);
    columns[col].end = event.end;
    layoutMap.set(event.instanceId, { column: col, totalColumns: 0 }); // fill totalColumns later
    placed = true;
    break;
    }
    }
    if (!placed) {
    columns.push({ events: [event], end: event.end });
    layoutMap.set(event.instanceId, { column: columns.length - 1, totalColumns: 0 });
    }
    }

    // 3. Second pass: determine max concurrent columns for each event
    for (const event of sorted) {
    const layout = layoutMap.get(event.instanceId)!;
    let maxCol = layout.column;
    for (const [otherId, otherLayout] of layoutMap) {
    const other = eventsById.get(otherId)!;
    if (other.start < event.end && other.end > event.start) {
    maxCol = Math.max(maxCol, otherLayout.column);
    }
    }
    layout.totalColumns = maxCol + 1;
    }

    return layoutMap;
    }

    // Render: left = (column / totalColumns) * 100%, width = (1 / totalColumns) * 100%
    ```

    ### 10.3 Caching & Prefetching

    ```typescript
    // React Query config
    const queryClient = new QueryClient({
    defaultOptions: {
    queries: {
    staleTime: 5 * 60 * 1000, // 5 min: don't refetch if fresh
    gcTime: 30 * 60 * 1000, // 30 min: keep in memory
    refetchOnWindowFocus: true,
    retry: 2,
    }
    }
    });

    // Prefetch adjacent weeks on navigation
    function prefetchAdjacentWeeks(date: Date) {
    [-1, 1].forEach(offset => {
    const adjacentWeek = addWeeks(date, offset);
    queryClient.prefetchQuery({
    queryKey: ['events', calendarId, getWeekRange(adjacentWeek)],
    queryFn: () => fetchEvents(calendarId, getWeekRange(adjacentWeek))
    });
    });
    }
    ```

    **IndexedDB cache:** Events are also persisted to IndexedDB via `idb-keyval` as the offline store. On first load, the app reads from IndexedDB (instant), then revalidates against the network in the background.

    ---

    ## 11. Real-Time Sync & Conflict Resolution

    **Sync strategy:** Google Calendar uses a **sync token** approach:

    ```typescript
    // Initial full sync
    const { items, nextSyncToken } = await calendarApi.list({ calendarId });
    storeSyncToken(calendarId, nextSyncToken);

    // Incremental sync (WebSocket message or polling)
    const { items: changed, nextSyncToken: newToken } = await calendarApi.list({
    calendarId,
    syncToken: getSyncToken(calendarId)
    });
    // changed contains only added/modified/deleted events since last sync
    applyIncrementalChanges(changed);
    storeSyncToken(calendarId, newToken);
    ```

    **Conflict resolution (Optimistic Concurrency Control):**

    Each event has an `etag` (server-assigned version hash) and a `sequence` (iCal integer).

    - Client sends update with `If-Match: <etag>` header
    - If server returns `412 Precondition Failed`, another client modified the event
    - Strategy: **last-write-wins for non-overlapping fields**, **merge-on-conflict for attendee lists**
    - If unresolvable: show conflict UI with diff viewer, let user pick version

    **WebSocket / SSE:**

    ```typescript
    // Server-Sent Events for push updates (simpler than WebSocket for calendar)
    const eventSource = new EventSource('/api/events/stream?token=...');

    eventSource.addEventListener('event.updated', (e) => {
    const updatedEvent = JSON.parse(e.data);
    queryClient.setQueryData(
    ['events', updatedEvent.calendarId, currentRange],
    (old: CalendarEvent[]) => mergeEvent(old, updatedEvent)
    );
    });

    eventSource.addEventListener('event.deleted', (e) => {
    const { id, calendarId } = JSON.parse(e.data);
    queryClient.setQueryData(
    ['events', calendarId, currentRange],
    (old: CalendarEvent[]) => old.filter(ev => ev.id !== id)
    );
    });
    ```

    ---

    ## 12. Offline Support & Service Workers

    **Service Worker strategy (Workbox):**

    | Resource | Cache Strategy |
    |---|---|
    | App Shell (HTML, CSS, JS) | Cache First (version-busted on deploy) |
    | Static assets (fonts, icons) | Stale While Revalidate |
    | Calendar event API responses | Network First with fallback to IndexedDB |
    | Profile images | Cache First with 24hr expiry |

    ```typescript
    // sw.ts
    import { registerRoute } from 'workbox-routing';
    import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';

    // API: network-first with IndexedDB fallback
    registerRoute(
    ({ url }) => url.pathname.startsWith('/api/events'),
    new NetworkFirst({
    cacheName: 'events-cache',
    plugins: [new ExpirationPlugin({ maxAgeSeconds: 60 * 60 * 24 })]
    })
    );
    ```

    **Offline event creation:**

    ```typescript
    // Queue mutations when offline
    const mutation = useMutation({
    mutationFn: createEvent,
    onMutate: async (newEvent) => {
    // 1. Optimistic update
    queryClient.setQueryData(['events', ...], (old) => [...old, newEvent]);
    return { previousEvents };
    },
    onError: (err, _, context) => {
    if (!navigator.onLine) {
    offlineQueue.push({ type: 'createEvent', payload: newEvent });
    return; // Don't rollback — will sync when back online
    }
    queryClient.setQueryData(['events', ...], context.previousEvents);
    }
    });

    // Flush queue on reconnect
    window.addEventListener('online', () => flushOfflineQueue());
    ```

    ---

    ## 13. Accessibility (a11y)

    ### ARIA Grid Pattern

    The week/day time grid implements the ARIA `grid` role with `row` and `gridcell` roles:

    ```html
    <div role="grid" aria-label="Week of June 10–16">
    <div role="row">
    <div role="columnheader" aria-label="Monday June 10">Mon 10</div>
    <!-- ... -->
    </div>
    <div role="row" aria-label="8:00 AM">
    <div role="gridcell" aria-label="Monday June 10, 8:00 AM, empty">
    <!-- Event blocks as buttons inside the cell -->
    <button role="button" aria-label="Standup, 8:00 to 8:30 AM" tabindex="0">
    Standup
    </button>
    </div>
    </div>
    </div>
    ```

    ### Keyboard Navigation

    | Key | Action |
    |---|---|
    | `Arrow keys` | Navigate between time slots |
    | `Enter` / `Space` | Open event detail |
    | `n` | New event at focused slot |
    | `e` | Edit focused event |
    | `Delete` / `Backspace` | Delete focused event (confirm dialog) |
    | `Escape` | Close modal / cancel drag |
    | `t` | Jump to today |
    | `1` / `2` / `3` | Switch Day / Week / Month view |

    ### Focus Management

    - Opening the event modal traps focus inside (`focus-trap-react`)
    - On modal close, focus returns to the triggering element
    - Drag-and-drop has a keyboard mode: activate dragging with `Space`, move with arrows, confirm with `Enter`, cancel with `Escape`

    ### Screen Reader Announcements

    ```typescript
    // aria-live region for dynamic announcements
    const [announcement, setAnnouncement] = useState('');

    // After drag completes:
    setAnnouncement(`Moved "${event.title}" to Monday June 12 at 2:00 PM`);

    // In render:
    <div role="status" aria-live="polite" className="sr-only">
    {announcement}
    </div>
    ```

    ---

    ## 14. API Contract

    ### REST Endpoints (Google Calendar API v3 pattern)

    ```
    GET /calendars/{calendarId}/events
    ?timeMin=&timeMax=&singleEvents=true&syncToken=
    → { items: Event[], nextSyncToken, nextPageToken }
    GET /calendars/{calendarId}/events/{eventId}
    → Event
    POST /calendars/{calendarId}/events
    Body: Partial<Event>
    → Event (with server-assigned id, etag)
    PUT /calendars/{calendarId}/events/{eventId}
    Header: If-Match: <etag>
    Body: Event
    → Event (with updated etag)
    PATCH /calendars/{calendarId}/events/{eventId}
    Body: Partial<Event> (field mask pattern)
    → Event
    DELETE /calendars/{calendarId}/events/{eventId}
    Header: If-Match: <etag>
    → 204 No Content
    POST /calendars/{calendarId}/events/{eventId}/move
    ?destination={calendarId}
    → Event (in new calendar)
    GET /freeBusy
    Body: { timeMin, timeMax, items: [{ id: calendarId }] }
    → { calendars: { [calendarId]: { busy: Period[] } } }
    ```

    ### WebSocket / SSE Events

    ```typescript
    type ServerEvent =
    | { type: 'event.created'; calendarId: string; event: CalendarEvent }
    | { type: 'event.updated'; calendarId: string; event: CalendarEvent }
    | { type: 'event.deleted'; calendarId: string; eventId: string }
    | { type: 'calendar.updated'; calendar: Calendar }
    | { type: 'notification.due'; notification: Notification };
    ```

    ---

    ## 15. Testing Strategy

    ### Pyramid

    ```
    E2E (Playwright, ~20 tests)
    ├── Create recurring event, verify next 4 weeks
    ├── Drag event across days, confirm API call
    ├── Offline create event, reconnect, verify sync
    └── Timezone change, verify event time updated
    Integration (React Testing Library, ~80 tests)
    ├── WeekView renders correct event positions
    ├── Recurrence engine produces correct instances
    ├── Drag-and-drop fires correct mutations
    └── Edit modal shows correct edit-mode dialog for recurring
    Unit (Vitest, ~200 tests)
    ├── parseRRule (all FREQ/BYDAY/BYSETPOS combos)
    ├── expandRecurrence (DST transitions, COUNT, UNTIL)
    ├── computeOverlapLayout (3-column cases, edge cases)
    ├── snapToSlot (15-min rounding)
    └── mergeExceptions (exception overrides master instances)
    ```

    ### Key Test Cases for Recurrence Engine

    ```typescript
    describe('expandRecurrence', () => {
    test('DAILY for 5 days from Jan 1', () => { /* count=5 */ });
    test('WEEKLY on MO,WE,FR', () => { /* byDay filter */ });
    test('last Friday of month for 3 months', () => { /* BYDAY=-1FR */ });
    test('excludes EXDATE entries', () => { /* skip specific date */ });
    test('handles spring-forward DST', () => { /* 2:00 AM → 3:00 AM */ });
    test('UNTIL truncates correctly', () => { /* doesn't include UNTIL date */ });
    test('merges server exception that moved instance', () => { /* override */ });
    });
    ```

    ---

    ## 16. Interview Cheat Sheet

    **Questions you will almost certainly be asked, and how to nail them:**

    ### "Walk me through the architecture"
    Start with data model → state layers → component tree → rendering strategies. Use the tier structure: server state (React Query) → global UI state (Zustand) → derived state (useMemo) → local state (useState).

    ### "How do you handle recurring events?"
    1. Server stores master + RRULE string only. Frontend expands instances for the visible range.
    2. Explain the sweep through the RRULE (FREQ → INTERVAL → BYDAY filters).
    3. Mention EXDATE for exceptions and the three edit modes.
    4. Show the cache key design to avoid redundant expansions.

    ### "How do you render overlapping events?"
    Describe the O(n log n) sweep line / interval scheduling algorithm. Key: sort by start time, assign to first available column (no overlap), second pass to count total concurrent columns, then set CSS `left` and `width` as percentages.

    ### "How would you make this fast?"
    - Virtual scrolling for agenda view
    - `content-visibility: auto` for off-screen month rows
    - React.memo + stable event ID arrays as props
    - Prefetch adjacent week/month on navigation
    - IndexedDB for instant cold-start load
    - Code-split each view (Day/Week/Month/Agenda are separate bundles)

    ### "How do you handle timezones?"
    Stress two points: (1) all-day events are date-only, never convert to UTC, (2) weekly recurring events across DST boundaries must compute each instance's local time independently, not add fixed milliseconds.

    ### "How would you support offline?"
    Service Worker (Workbox) caches API responses. Mutations while offline are queued in `localStorage` / IndexedDB. On reconnect, the offline queue is flushed in order. Conflict detection via `etag` + `sequence`.

    ### "What are your non-functional requirements?"
    Lead with the 60fps constraint for drag-and-drop, then < 3s TTI, then < 2s real-time sync. Always quantify.

    ### "What would you do differently at 10x scale?"
    - Push RRULE expansion to the server with pagination (cursor-based)
    - Switch to a canvas-based rendering engine for the time grid (like Google Calendar itself does for performance at scale)
    - Use a CRDT (e.g. Yjs) instead of OCC for true real-time collaborative editing

    ---

    *This document covers the frontend system design comprehensively enough to pass system design rounds at Microsoft, Amazon, Uber, Google, Meta, and similar companies. Focus on the data model and recurrence engine — they are the most differentiating areas that separate strong candidates from the rest.*