Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

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

Select an option

Save carefree-ladka/521dbd1f16f6e7c390b4bc2be8240b3b to your computer and use it in GitHub Desktop.
Frontend System Design: Google Calendar

Frontend System Design: Google Calendar

A deep-dive reference for designing a production-grade calendar application — covering data modelling, architecture, event rendering, APIs, and optimization.


Table of Contents

  1. Overview
  2. Functional Requirements
  3. Non-Functional Requirements
  4. Data Modelling
  5. High-Level Architecture
  6. View System Design
  7. Event Rendering Techniques
  8. Drag & Drop System
  9. Recurrence Engine
  10. API Interfaces
  11. Caching & Data Fetching Strategy
  12. Performance Optimizations
  13. Accessibility (a11y)
  14. Offline Support & PWA
  15. Security Considerations
  16. Scalability & Edge Cases
  17. Testing Strategy
  18. Observability & Monitoring

Overview

A Google Calendar clone is a deceptively complex frontend system. On the surface it's a grid of colored boxes. Underneath, it demands:

  • Precise temporal reasoning across timezones, DST, and recurrence rules
  • Dense visual layouts with non-overlapping event collision resolution
  • Real-time collaboration with optimistic UI and conflict resolution
  • High performance even with thousands of events rendered across views

This document walks through every layer of the system — from raw data models to rendering algorithms to API boundaries — as you would design it in a senior frontend system design interview or an actual production build.


Functional Requirements

Core Features

# Feature Description
F1 Multiple Views Day, Week, Month, and Agenda/List views
F2 Event CRUD Create, read, update, delete calendar events
F3 Event Details Title, description, start/end datetime, location, color, guests, attachments
F4 Recurring Events Support RRULE patterns: daily, weekly, monthly, yearly, custom
F5 Multi-calendar Multiple calendars per user (work, personal, shared, holidays)
F6 Drag & Drop Drag events to reschedule; resize to change duration
F7 Search Full-text event search across past and future
F8 Timezone Support Display events in any timezone; detect user's local timezone
F9 Sharing & Collaboration Share calendars, invite guests, show guest RSVP status
F10 Notifications Browser notifications and in-app reminders
F11 Import/Export Import .ics files; export events to .ics
F12 Keyboard Navigation Full keyboard shortcuts (n = next, p = prev, t = today, 1/2/3/4 = view change)

Stretch Features

  • Google Meet / video conferencing link generation
  • Smart scheduling ("Find a time" based on attendees' free/busy)
  • Event color coding and categories
  • Birthdays and task integration
  • Mini-calendar side panel

Non-Functional Requirements

Performance

Metric Target
First Contentful Paint (FCP) < 1.5s
Time to Interactive (TTI) < 3s
View switch latency < 100ms (with cached data)
Event render (1000 events) < 16ms frame budget
API response (event fetch) < 200ms (p95)

Reliability

  • Availability: 99.9% uptime (< 8.7h downtime/year)
  • Offline: Read-only access to cached events; queued writes synced on reconnect
  • Optimistic UI: UI updates instantly; rolls back on API failure

Scalability

  • Handle users with 10,000+ events across 5+ years of history
  • 50+ calendars (shared team calendars, holiday feeds)
  • Real-time sync for 10–20 concurrent collaborators on a shared calendar

Compatibility

  • Modern browsers: Chrome, Firefox, Safari, Edge (last 2 major versions)
  • Mobile-responsive down to 375px wide
  • WCAG 2.1 AA accessibility compliance

Data Modelling

Core Entities

┌─────────────────────┐        ┌──────────────────────┐
│        User         │        │       Calendar       │
├─────────────────────┤        ├──────────────────────┤
│ id: string          │1─────N│ id: string            │
│ email: string       │        │ ownerId: string       │
│ displayName: string │        │ name: string          │
│ photoUrl: string    │        │ color: string         │
│ timezone: string    │        │ isVisible: boolean    │
│ settings: UserPrefs │        │ accessRole: ACLRole   │
└─────────────────────┘        └──────────────────────┘
                                          │
                                          │ 1─────N
                                          ▼
                              ┌──────────────────────────┐
                              │          Event            │
                              ├──────────────────────────┤
                              │ id: string                │
                              │ calendarId: string        │
                              │ title: string             │
                              │ description: string       │
                              │ start: EventDateTime      │
                              │ end: EventDateTime        │
                              │ isAllDay: boolean         │
                              │ recurrence: string[]      │ ← RRULE strings
                              │ recurringEventId?: string │ ← parent ID
                              │ originalStartTime?: DT    │ ← exception key
                              │ status: EventStatus       │
                              │ color?: string            │
                              │ location?: string         │
                              │ attendees: Attendee[]     │
                              │ reminders: Reminder[]     │
                              │ attachments: Attachment[] │
                              │ creator: EventActor       │
                              │ organizer: EventActor     │
                              │ etag: string              │ ← optimistic lock
                              │ updated: string           │ ← ISO 8601
                              └──────────────────────────┘

TypeScript Interfaces

// ─── Primitives ─────────────────────────────────────────────

type ISODateTime = string; // "2025-08-14T09:00:00+05:30"
type ISODate     = string; // "2025-08-14"
type HexColor    = string; // "#3788d8"

// ─── Event DateTime ─────────────────────────────────────────

interface EventDateTime {
  dateTime?: ISODateTime; // for timed events
  date?: ISODate;         // for all-day events
  timeZone?: string;      // IANA tz identifier, e.g. "Asia/Kolkata"
}

// ─── Attendee ───────────────────────────────────────────────

type RSVPStatus = 'accepted' | 'declined' | 'tentative' | 'needsAction';

interface Attendee {
  email: string;
  displayName?: string;
  self?: boolean;         // true if this is the requesting user
  organizer?: boolean;
  responseStatus: RSVPStatus;
  optional?: boolean;
}

// ─── Reminder ───────────────────────────────────────────────

type ReminderMethod = 'email' | 'popup';

interface Reminder {
  method: ReminderMethod;
  minutes: number;        // minutes before event start
}

// ─── Event ──────────────────────────────────────────────────

type EventStatus = 'confirmed' | 'tentative' | 'cancelled';
type EventVisibility = 'default' | 'public' | 'private' | 'confidential';

interface CalendarEvent {
  id: string;
  calendarId: string;
  title: string;               // "summary" in Google API
  description?: string;
  location?: string;
  start: EventDateTime;
  end: EventDateTime;
  isAllDay: boolean;
  recurrence?: string[];       // ["RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"]
  recurringEventId?: string;   // parent series ID for exceptions
  originalStartTime?: EventDateTime;
  status: EventStatus;
  visibility: EventVisibility;
  color?: HexColor;
  calendarColor?: HexColor;    // resolved fallback from Calendar
  attendees: Attendee[];
  reminders: { useDefault: boolean; overrides?: Reminder[] };
  attachments?: Attachment[];
  creator: EventActor;
  organizer: EventActor;
  htmlLink: string;
  etag: string;
  created: ISODateTime;
  updated: ISODateTime;
  // ── Client-only fields ──
  _isPlaceholder?: boolean;    // optimistic insert
  _computedLane?: number;      // collision resolution result
  _computedLaneCount?: number;
}

// ─── Calendar ───────────────────────────────────────────────

type ACLRole = 'none' | 'freeBusyReader' | 'reader' | 'writer' | 'owner';

interface Calendar {
  id: string;
  summary: string;
  description?: string;
  timeZone: string;
  color: HexColor;
  backgroundColor: HexColor;
  foregroundColor: HexColor;
  accessRole: ACLRole;
  selected: boolean;
  primary?: boolean;
}

Recurrence Rule (RRULE)

Recurring events follow RFC 5545 iCalendar RRULE format:

RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20251231T235959Z

RRULE:FREQ=MONTHLY;BYDAY=2MO           ← 2nd Monday of every month
RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1  ← Jan 1st every year
RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10   ← every other day, 10 times

Expansion Strategy: Never store expanded instances. Generate them on-demand using a library like rrule.js. Cache the expanded set per viewport window.

import { RRule, RRuleSet } from 'rrule';

function expandRecurringEvent(
  event: CalendarEvent,
  windowStart: Date,
  windowEnd: Date
): CalendarEvent[] {
  if (!event.recurrence?.length) return [event];

  const set = new RRuleSet();
  for (const rule of event.recurrence) {
    if (rule.startsWith('RRULE:')) set.rrule(RRule.fromString(rule));
    if (rule.startsWith('EXDATE:')) set.exdate(parseExDate(rule));
  }

  return set
    .between(windowStart, windowEnd, true)
    .map((occurrenceDate, i) => ({
      ...event,
      id: `${event.id}_${occurrenceDate.toISOString()}`,
      recurringEventId: event.id,
      originalStartTime: { dateTime: occurrenceDate.toISOString() },
      start: offsetEventDateTime(event.start, occurrenceDate),
      end:   offsetEventDateTime(event.end,   occurrenceDate),
    }));
}

High-Level Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Browser (Client)                         │
│                                                                 │
│  ┌───────────┐  ┌──────────────────────────────────────────┐   │
│  │  Router   │  │              App Shell                   │   │
│  │ /calendar │  │  Sidebar  │  Header (Nav/View Toggle)    │   │
│  │ /event    │  ├───────────┴──────────────────────────────┤   │
│  └───────────┘  │           Calendar Viewport              │   │
│                 │  ┌──────────────────────────────────────┐│   │
│                 │  │  View Controller (Day/Week/Month/     ││   │
│                 │  │  Agenda) ─ driven by URL params       ││   │
│                 │  │  ┌──────────────────────────────────┐ ││   │
│                 │  │  │  Time Grid / Month Grid          │ ││   │
│                 │  │  │  EventLayer (collision-resolved) │ ││   │
│                 │  │  │  DragDropLayer                   │ ││   │
│                 │  │  └──────────────────────────────────┘ ││   │
│                 │  └──────────────────────────────────────┘│   │
│                 └──────────────────────────────────────────┘   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                   State Layer                           │   │
│  │  CalendarStore  EventStore  UIStore  AuthStore          │   │
│  │  (Zustand / Redux Toolkit / Jotai)                      │   │
│  └────────────────────────┬────────────────────────────────┘   │
│                           │                                     │
│  ┌────────────────────────▼────────────────────────────────┐   │
│  │               Data / Sync Layer                         │   │
│  │  React Query / SWR   ──  API Client  ──  WS Client      │   │
│  │  IndexedDB (offline cache)  ──  Service Worker          │   │
│  └────────────────────────┬────────────────────────────────┘   │
└───────────────────────────┼─────────────────────────────────────┘
                            │ HTTPS / WSS
┌───────────────────────────▼─────────────────────────────────────┐
│                     Backend Services                            │
│  API Gateway ── Calendar Service ── Notification Service        │
│  Auth Service ── Search Service ── Recurring Engine             │
│  PostgreSQL ── Redis (pub/sub) ── Elasticsearch                  │
└─────────────────────────────────────────────────────────────────┘

Component Hierarchy

<App>
  <AuthProvider>
    <ThemeProvider>
      <Router>
        <AppShell>
          <Sidebar>
            <MiniCalendar />              ← small nav calendar
            <CalendarList />              ← toggleable calendars
            <CreateEventButton />
          </Sidebar>
          <MainContent>
            <CalendarHeader>
              <DateNavigation />          ← prev/next/today
              <ViewToggle />              ← day/week/month/agenda
              <SearchBar />
              <SettingsMenu />
            </CalendarHeader>
            <CalendarViewport>
              {view === 'day'    && <DayView />}
              {view === 'week'   && <WeekView />}
              {view === 'month'  && <MonthView />}
              {view === 'agenda' && <AgendaView />}
            </CalendarViewport>
          </MainContent>
        </AppShell>
        <EventModal />                    ← portal, renders above all
        <EventPopover />                  ← quick-view popover
      </Router>
    </ThemeProvider>
  </AuthProvider>
</App>

State Management Architecture

Use Zustand slices (or Redux Toolkit slices) with React Query for server state:

// ── UI State (client-only) ───────────────────────────────────────
interface UIStore {
  currentView: 'day' | 'week' | 'month' | 'agenda';
  focusedDate: Date;
  selectedEventId: string | null;
  isDragging: boolean;
  // actions
  setView(view: ViewType): void;
  navigateDate(delta: number): void;
  selectEvent(id: string | null): void;
}

// ── Calendar State ───────────────────────────────────────────────
interface CalendarStore {
  calendars: Map<string, Calendar>;
  visibleCalendarIds: Set<string>;
  toggleCalendarVisibility(id: string): void;
}

// ── Event State — largely managed by React Query ─────────────────
// useEvents(startDate, endDate) → paginated + expanded recurring
// useEvent(id)                  → single event details
// useMutateEvent()              → create/update/delete with optimistic updates

View System Design

Day View

A single day rendered as a 24-hour vertical time grid.

┌──────────────────────────────────┐
│  all-day zone (sticky header)    │ ← fixed height, scrolls independently
├────┬─────────────────────────────┤
│ 6  │                             │ ← 1 column = 1 hour = HOUR_HEIGHT px
│    │  ███████████████            │ ← Event: top/height from start/end
│ 7  │  Meeting (9:00–10:00)       │
│    │  ███████████████            │
│ 8  │           ████████████████ │ ← Overlapping event: lane system
│    │           Standup (8:30–9)  │
└────┴─────────────────────────────┘

Week View

Seven day columns, each with the same time grid. The most complex view.

interface WeekViewProps {
  weekStart: Date;     // Monday or Sunday based on locale
  events: CalendarEvent[];
}

// Layout algorithm:
// 1. Partition events by day column
// 2. Within each column, run collision detection
// 3. Assign lane (column subset) to each overlapping event group
// 4. Position each event: left = (lane / laneCount) * 100%
//                         width = (1 / laneCount) * 100%

Month View

A 6-row × 7-column grid. Events are rendered as compact chips.

  • Maximum N visible events per cell (typically 3–4), with "+X more" overflow
  • Multi-day events span columns using absolute positioning in an overlay layer
  • "Filler" days from prev/next month shown with reduced opacity

Agenda / List View

A flat, chronological list of upcoming events grouped by date. Ideal for mobile and accessibility.

interface AgendaGroup {
  date: ISODate;
  events: CalendarEvent[];
}
// Virtualised with react-window or tanstack-virtual for long lists

Event Rendering Techniques

This is the most algorithmically interesting part of the system.

Time-Grid Positioning

Every timed event maps to CSS top + height within its column:

const HOUR_HEIGHT = 60;      // px per hour (configurable)
const DAY_START   = 0;       // 0 = midnight, 6 = 6 AM

function eventStyle(event: CalendarEvent): CSSProperties {
  const startMins = getMinutesSinceDayStart(event.start);
  const endMins   = getMinutesSinceDayStart(event.end);
  const duration  = Math.max(endMins - startMins, 30); // min render height

  return {
    position: 'absolute',
    top:    `${(startMins / 60) * HOUR_HEIGHT}px`,
    height: `${(duration  / 60) * HOUR_HEIGHT}px`,
  };
}

function getMinutesSinceDayStart(dt: EventDateTime): number {
  const d = parseToLocalDate(dt); // respects timezone
  return d.getHours() * 60 + d.getMinutes();
}

Collision Detection & Lane Layout

When events overlap in time, they must be laid out side-by-side:

interface EventGroup {
  events: CalendarEvent[];
  laneCount: number;
}

/**
 * Groups overlapping events into collision clusters,
 * assigns each a lane index for horizontal layout.
 */
function resolveCollisions(events: CalendarEvent[]): CalendarEvent[] {
  // 1. Sort by start time, then by duration (longer first)
  const sorted = [...events].sort((a, b) => {
    const startDiff = getStart(a) - getStart(b);
    return startDiff !== 0 ? startDiff : getDuration(b) - getDuration(a);
  });

  const columns: CalendarEvent[][] = [];

  for (const event of sorted) {
    // Find the first column where the last event doesn't overlap
    let placed = false;
    for (let col = 0; col < columns.length; col++) {
      const last = columns[col].at(-1)!;
      if (getEnd(last) <= getStart(event)) {
        columns[col].push(event);
        event._computedLane = col;
        placed = true;
        break;
      }
    }
    if (!placed) {
      event._computedLane = columns.length;
      columns.push([event]);
    }
  }

  // 2. Determine max concurrent lanes per event (for width calc)
  for (const event of sorted) {
    const overlapping = sorted.filter(
      e => e !== event && overlaps(event, e)
    );
    event._computedLaneCount = Math.max(
      event._computedLane! + 1,
      ...overlapping.map(e => (e._computedLane ?? 0) + 1)
    );
  }

  return sorted;
}

// CSS from lane data:
function laneStyle(event: CalendarEvent, gapPx = 2): CSSProperties {
  const lane  = event._computedLane ?? 0;
  const total = event._computedLaneCount ?? 1;
  return {
    left:  `calc(${(lane  / total) * 100}% + ${gapPx}px)`,
    width: `calc(${(1     / total) * 100}% - ${gapPx * 2}px)`,
  };
}

All-Day Event Rendering

All-day events live in a separate sticky header zone above the time grid. They're sorted by duration (longer = higher priority = top row) and assigned rows when they overlap.

function layoutAllDayEvents(
  events: CalendarEvent[],
  weekStart: Date
): AllDayEventLayout[] {
  const rows: CalendarEvent[][] = [];
  const sorted = [...events].sort(
    (a, b) => getDaySpan(b) - getDaySpan(a)
  );

  for (const event of sorted) {
    let rowIndex = rows.findIndex(row =>
      !row.some(e => dayRangesOverlap(e, event))
    );
    if (rowIndex === -1) {
      rowIndex = rows.length;
      rows.push([]);
    }
    rows[rowIndex].push(event);

    const startCol = daysBetween(weekStart, getStartDate(event));
    const span     = Math.min(getDaySpan(event), 7 - startCol);

    layouts.push({ event, row: rowIndex, startCol, span });
  }
  return layouts;
}

Multi-Day Spanning Events

In month view, multi-day events are rendered in an absolutely-positioned overlay grid that floats above the day cells:

┌──────────────────────────────────────────────────────────┐
│ Mon  │ Tue  │ Wed  │ Thu  │ Fri  │ Sat  │ Sun            │
├──────────────────────────────────────────────────────────┤
│ ███████████████████████████████ Conference (Mon–Thu)     │
│      │ ████ Solo standup                                  │
│  12  │  13  │  14  │  15  │  16  │  17  │  18            │
└──────────────────────────────────────────────────────────┘

Position: left  = (startDayIndex / 7) * 100%
           width = (spanDays     / 7) * 100%
           top   = rowIndex * EVENT_ROW_HEIGHT

Pixel-Perfect Positioning Formula

// Week/Day View — absolute positioning in time column
const top    = (startMinutes / (24 * 60)) * GRID_HEIGHT;
const height = (durationMins / (24 * 60)) * GRID_HEIGHT;
const left   = (lane / laneCount) * COLUMN_WIDTH;
const width  = (1   / laneCount) * COLUMN_WIDTH - GAP;

// Month View — CSS grid, events as overlay
const gridColumnStart = dayOfWeek + 1;       // 1-indexed CSS grid
const gridColumnEnd   = gridColumnStart + span;

Drag & Drop System

User grabs event
      │
      ▼
onDragStart: snapshot original position + create ghost element
      │
      ▼
onDragOver (throttled @ 60fps via requestAnimationFrame):
  - compute target date/time from cursor position
  - snap to 15-minute increments
  - show drop preview (placeholder event with dashed border)
      │
      ▼
onDrop:
  - optimistically update UI
  - fire PATCH /events/{id} with new start/end
  - on error: rollback to snapshot + show toast

Implementation Notes:

  • Use the HTML5 Drag and Drop API or a library like dnd-kit for accessibility compliance
  • Touch support requires pointer events approach (not native DnD)
  • Resize handles at the bottom of each event modify end time only
  • Snap granularity: 15 min for drag, 5 min for resize
function snapToGrid(minutes: number, snapMinutes = 15): number {
  return Math.round(minutes / snapMinutes) * snapMinutes;
}

function cursorToTime(
  cursorY: number,
  gridRef: HTMLElement,
  gridStartHour = 0
): Date {
  const rect    = gridRef.getBoundingClientRect();
  const relY    = cursorY - rect.top + gridRef.scrollTop;
  const minutes = (relY / HOUR_HEIGHT) * 60 + gridStartHour * 60;
  return minutesToDate(snapToGrid(minutes));
}

Recurrence Engine

Edit Modes

When editing a recurring event, present three options:

┌──────────────────────────────────────┐
│  Edit recurring event                │
│                                      │
│  ○ This event                        │ → creates exception (EXDATE + new instance)
│  ○ This and following events         │ → splits RRULE UNTIL; creates new series
│  ○ All events                        │ → modifies the master event
└──────────────────────────────────────┘

Exception Handling

// Modifying a single instance:
// 1. Add EXDATE to the master event
// 2. Create a new standalone event with recurringEventId pointing to master

// Master event gets:
EXDATE:20250815T090000Z

// New exception event:
{
  recurringEventId: "master_id",
  originalStartTime: { dateTime: "2025-08-15T09:00:00Z" },
  start: { dateTime: "2025-08-15T10:00:00Z" }, // moved to 10 AM
  ...rest
}

Timezone-Safe Expansion

When expanding recurrences near DST transitions, always expand in the event's declared timezone (not UTC or local), then convert for display:

import { RRule } from 'rrule';

const rule = new RRule({
  freq: RRule.WEEKLY,
  dtstart: new Date('2025-01-06T09:00:00'),  // local time, no Z suffix
  tzid: 'America/New_York',
  byday: [RRule.MO, RRule.WE, RRule.FR],
  until: new Date('2025-12-31T09:00:00'),
});

API Interfaces

REST API Contracts

The API closely mirrors the Google Calendar API v3 shape:

Events

GET  /calendars/{calendarId}/events
     ?timeMin=2025-08-01T00:00:00Z
     &timeMax=2025-08-31T23:59:59Z
     &singleEvents=true          ← expand recurrences
     &orderBy=startTime
     &maxResults=250
     &pageToken={token}          ← cursor pagination

POST   /calendars/{calendarId}/events
PATCH  /calendars/{calendarId}/events/{eventId}
PUT    /calendars/{calendarId}/events/{eventId}
DELETE /calendars/{calendarId}/events/{eventId}
       ?sendUpdates=all|externalOnly|none

Batch Endpoint

POST /batch
Content-Type: multipart/mixed; boundary=batch_boundary

--batch_boundary
Content-Type: application/http
GET /calendars/primary/events?timeMin=...

--batch_boundary
Content-Type: application/http
GET /calendars/work@group.calendar.google.com/events?timeMin=...
--batch_boundary--

Calendar List

GET    /users/me/calendarList
POST   /users/me/calendarList           ← subscribe to calendar
PATCH  /users/me/calendarList/{id}      ← update color/visibility
DELETE /users/me/calendarList/{id}      ← unsubscribe

Response Shape

interface EventListResponse {
  kind: 'calendar#events';
  summary: string;
  updated: ISODateTime;
  items: CalendarEvent[];
  nextPageToken?: string;   // cursor for next page
  nextSyncToken?: string;   // use for incremental sync
}

Incremental Sync with syncToken

// First full sync
const res1 = await fetchEvents({ timeMin, timeMax });
const { items, nextSyncToken } = res1;

// Save nextSyncToken. On next poll:
const res2 = await fetchEvents({ syncToken: nextSyncToken });
// res2.items contains ONLY changed/deleted events since last sync
// deleted events have status: 'cancelled'

GraphQL Schema (Optional)

For clients needing flexible queries (e.g., mobile apps):

type Query {
  calendars: [Calendar!]!
  events(
    calendarId: ID!
    timeMin: DateTime!
    timeMax: DateTime!
    singleEvents: Boolean = true
  ): EventConnection!
  event(calendarId: ID!, eventId: ID!): Event
}

type Mutation {
  createEvent(calendarId: ID!, input: EventInput!): Event!
  updateEvent(calendarId: ID!, eventId: ID!, input: EventInput!): Event!
  deleteEvent(calendarId: ID!, eventId: ID!, sendUpdates: SendUpdates): Boolean!
  moveEvent(eventId: ID!, from: ID!, to: ID!): Event!
}

type Subscription {
  eventChanged(calendarId: ID!): EventChangePayload!
}

WebSocket / Real-Time Events

For collaborative calendars, use WebSocket push notifications:

// Server → Client messages
type ServerMessage =
  | { type: 'EVENT_CREATED'; payload: CalendarEvent }
  | { type: 'EVENT_UPDATED'; payload: Partial<CalendarEvent> & { id: string } }
  | { type: 'EVENT_DELETED'; payload: { id: string; calendarId: string } }
  | { type: 'CALENDAR_UPDATED'; payload: Partial<Calendar> & { id: string } }
  | { type: 'SYNC_REQUIRED'; payload: { syncToken: string } };

// Client subscribes by calendar
ws.send(JSON.stringify({
  action: 'SUBSCRIBE',
  calendarIds: ['primary', 'work@group.calendar.google.com'],
}));

Alternatively, use Server-Sent Events (SSE) for simpler one-way push:

GET /calendars/watch
Accept: text/event-stream

data: {"type":"EVENT_UPDATED","payload":{...}}
data: {"type":"EVENT_DELETED","payload":{"id":"abc123"}}

Caching & Data Fetching Strategy

Fetch Window Strategy

Never fetch all historical events. Fetch per viewport window + buffer:

function getViewportWindow(view: ViewType, focusDate: Date) {
  switch (view) {
    case 'day':    return { start: startOfDay(focusDate),
                            end:   endOfDay(focusDate) };
    case 'week':   return { start: startOfWeek(focusDate),
                            end:   endOfWeek(focusDate) };
    case 'month':  return { start: startOfMonth(focusDate),
                            end:   addDays(endOfMonth(focusDate), 7) }; // show trailing week
  }
}

// Pre-fetch adjacent windows on idle
requestIdleCallback(() => prefetchAdjacentWindows(view, focusDate));

React Query Setup

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime:        5 * 60 * 1000,  // 5 min — events rarely change
      gcTime:          30 * 60 * 1000,  // 30 min cache retention
      refetchOnWindowFocus: true,
      retry: 2,
    },
  },
});

function useEvents(calendarId: string, start: Date, end: Date) {
  return useQuery({
    queryKey: ['events', calendarId, start.toISOString(), end.toISOString()],
    queryFn: () => fetchEvents({ calendarId, timeMin: start, timeMax: end }),
    placeholderData: keepPreviousData,  // no flicker on date navigation
  });
}

Optimistic Updates

const mutation = useMutation({
  mutationFn: (event: CalendarEvent) => api.updateEvent(event),
  onMutate: async (updated) => {
    await queryClient.cancelQueries({ queryKey: ['events'] });
    const previous = queryClient.getQueryData(['events', ...]);

    // Optimistically update cache
    queryClient.setQueryData(['events', ...], (old) =>
      old.map(e => e.id === updated.id ? updated : e)
    );

    return { previous }; // snapshot for rollback
  },
  onError: (err, updated, context) => {
    queryClient.setQueryData(['events', ...], context.previous);
    toast.error('Failed to update event. Changes reverted.');
  },
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['events'] }),
});

IndexedDB for Offline Cache

// Use Dexie.js for a clean IndexedDB API
const db = new Dexie('CalendarDB');
db.version(1).stores({
  events:    '++id, calendarId, start.dateTime, end.dateTime, updated',
  calendars: '++id',
  syncState: '++id, calendarId, syncToken',
});

// Service Worker intercepts API calls:
// ONLINE  → fetch from network, write-through to IndexedDB
// OFFLINE → serve from IndexedDB, queue mutations in outbox

Performance Optimizations

Windowing & Virtualization

For Agenda view or search results with 1000s of events, use virtual lists:

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

function AgendaList({ events }: { events: CalendarEvent[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  const virtualizer = useVirtualizer({
    count: events.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 72,   // px per event row estimate
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: '100%', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(item => (
          <div
            key={item.index}
            style={{ transform: `translateY(${item.start}px)` }}
          >
            <EventRow event={events[item.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Memoization Strategy

// Expensive: recurrence expansion + collision resolution
const resolvedEvents = useMemo(
  () => resolveCollisions(expandAllRecurring(rawEvents, viewStart, viewEnd)),
  [rawEvents, viewStart.toISOString(), viewEnd.toISOString()]
);

// Event chip component: skip re-render if event data unchanged
const EventChip = memo(({ event }: { event: CalendarEvent }) => {
  return <div style={eventStyle(event)}>{event.title}</div>;
}, (prev, next) => prev.event.etag === next.event.etag);

Bundle Splitting

// Split heavy date/rrule libraries into async chunks
const { RRule } = await import(/* webpackChunkName: "rrule" */ 'rrule');

// Lazy-load views
const MonthView = lazy(() => import('./views/MonthView'));
const AgendaView = lazy(() => import('./views/AgendaView'));

// Lazy-load event modal (large form)
const EventModal = lazy(() => import('./modals/EventModal'));

CSS Containment

Apply contain: strict on calendar grid cells to limit browser layout recalculation scope:

.calendar-cell {
  contain: strict;           /* layout, paint, size all contained */
  content-visibility: auto;  /* skip off-screen cells entirely */
}

.time-grid-column {
  contain: layout paint;
  will-change: transform;    /* promote to GPU layer during drag */
}

Debounce & Throttle Patterns

// Throttle scroll handler to once per frame
const handleScroll = useCallback(
  throttle(() => syncTimeIndicator(), 16),
  []
);

// Debounce search input
const handleSearch = useMemo(
  () => debounce((q: string) => searchEvents(q), 300),
  []
);

Accessibility (a11y)

ARIA Roles & Attributes

<div role="grid" aria-label="Calendar week view" aria-rowcount={25}>
  <div role="row">
    {days.map(day => (
      <div role="columnheader" aria-label={format(day, 'EEEE, MMMM d')}>
        {format(day, 'EEE')}
        <span aria-hidden="true">{format(day, 'd')}</span>
      </div>
    ))}
  </div>
  {hours.map(hour => (
    <div role="row" aria-label={format(hour, 'h a')}>
      {days.map(day => (
        <div
          role="gridcell"
          aria-label={`${format(day, 'EEEE')} ${format(hour, 'h a')}`}
          onClick={() => openNewEventDialog(day, hour)}
          onKeyDown={e => e.key === 'Enter' && openNewEventDialog(day, hour)}
          tabIndex={0}
        />
      ))}
    </div>
  ))}
</div>

// Events as interactive elements
<button
  role="button"
  aria-label={`${event.title}, ${formatEventTime(event)}`}
  aria-haspopup="dialog"
  aria-expanded={isOpen}
  onClick={() => openEventDetail(event)}
>
  <span aria-hidden="true">{event.title}</span>
</button>

Keyboard Navigation

Key Action
t Go to today
n Next period
p Previous period
1 Day view
2 Week view
3 Month view
4 Agenda view
c Create new event
Esc Close modal/popover
Tab Navigate between events
Enter Open event details
Delete Delete focused event

Offline Support & PWA

Architecture

┌─────────────────────────────────┐
│         Service Worker          │
│                                 │
│  Cache Strategy:                │
│  • App shell → Cache First      │
│  • Event API → Network First    │
│    (fallback to IndexedDB)      │
│  • Static assets → Stale While  │
│    Revalidate                   │
│                                 │
│  Background Sync:               │
│  • Outbox queue for mutations   │
│  • Retry on reconnect           │
└─────────────────────────────────┘

Background Sync

// Queue mutation when offline
async function queueMutation(mutation: PendingMutation) {
  await db.outbox.add({ ...mutation, timestamp: Date.now() });
  await navigator.serviceWorker.ready;
  await registration.sync.register('sync-calendar-mutations');
}

// SW sync handler
self.addEventListener('sync', event => {
  if (event.tag === 'sync-calendar-mutations') {
    event.waitUntil(flushOutbox());
  }
});

Security Considerations

Concern Mitigation
Auth OAuth 2.0 with PKCE; short-lived access tokens + refresh tokens; tokens in httpOnly cookies, never localStorage
XSS Sanitize all user content (event title/description) with DOMPurify before rendering; CSP headers
CSRF SameSite cookies; CSRF token on state-mutating requests
Data leakage Calendar permissions checked server-side; ACL enforced per calendar, not just per event
Event injection Validate RRULE strings server-side; cap recurrence expansion count (e.g., max 1,000 instances)
Rate limiting API gateway rate limits per user; exponential backoff on 429 responses
Content Security Policy default-src 'self'; script-src 'self' 'nonce-{...}' — no inline scripts

Scalability & Edge Cases

Edge Cases to Handle

Edge Case Handling Strategy
Midnight-spanning events Clip to 24:00 in day view; show continuation indicator next day
Short events (< 30 min) Enforce minimum render height (30 min); show truncated title
Overlapping all-day events Row-based stacking with max row limit + overflow count
Event at exactly midnight Treat as start of day (00:00), not end of previous day
DST transitions Always expand recurrences in event's declared IANA timezone
Leap year Feb 29 YEARLY recurrence skips Feb 29 in non-leap years unless BYMONTHDAY=-1
Very long event titles Truncate with ellipsis; full title in aria-label and tooltip
Events spanning weeks Split rendering at week boundary; show continuation arrows
1000+ events in month Virtual scroll in overflow popover; server-side count aggregation
Calendar feed deleted Gracefully show "Calendar unavailable" placeholder; retain events in read-only

Timezone Architecture

// Principle: store everything in UTC; display in user's preferred timezone

function displayTime(event: CalendarEvent, userTz: string): string {
  const dt = parseISO(event.start.dateTime!);
  return formatInTimeZone(dt, userTz, 'h:mm a');
}

// Multi-timezone support (for travelers / international teams)
interface AppSettings {
  primaryTimezone: string;        // "Asia/Kolkata"
  additionalTimezones?: string[]; // ["America/New_York", "Europe/London"]
}
// Render additional timezone labels on the left of the time grid

Testing Strategy

Unit Tests (Jest + Vitest)

  • resolveCollisions() — various overlapping permutations
  • expandRecurringEvent() — RRULE edge cases (DST, leap year, EXDATE)
  • snapToGrid() — boundary conditions
  • getMinutesSinceDayStart() — timezone-aware

Integration Tests (React Testing Library)

  • Create event flow: open modal → fill form → submit → event appears in grid
  • Drag & drop: simulate pointer events → verify event moved to new time
  • View navigation: click next/prev → verify correct date range rendered
  • Recurrence edit: "this event only" vs "all events" — verify correct API calls

E2E Tests (Playwright / Cypress)

test('create recurring weekly event', async ({ page }) => {
  await page.goto('/calendar');
  await page.click('[data-testid="create-event-btn"]');
  await page.fill('[name="title"]', 'Team Standup');
  await page.check('[name="repeat"]');
  await page.selectOption('[name="freq"]', 'WEEKLY');
  await page.click('[data-testid="save-event"]');

  // Navigate to next week
  await page.click('[data-testid="next-period"]');
  await expect(page.locator('text=Team Standup')).toBeVisible();
});

Visual Regression Tests (Storybook + Chromatic)

Stories for:

  • EventChip in all states (normal, past, all-day, multi-day, tentative, cancelled)
  • Collision layouts (2-up, 3-up, 4-up)
  • Month view with overflow (+N more)
  • Loading and empty states

Observability & Monitoring

Client-Side Metrics

// Core Web Vitals
import { onCLS, onFID, onLCP } from 'web-vitals';
onLCP(metric => analytics.track('web_vital', { name: 'LCP', value: metric.value }));

// Custom performance marks
performance.mark('view-switch-start');
// ... after render:
performance.measure('view-switch-duration', 'view-switch-start');
const [measure] = performance.getEntriesByName('view-switch-duration');
analytics.track('view_switch', { view: newView, duration: measure.duration });

Error Tracking

// Sentry integration
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
  integrations: [
    new Sentry.BrowserTracing({
      routingInstrumentation: Sentry.reactRouterV6Instrumentation(useNavigate),
    }),
  ],
});

// Capture failed mutations with context
mutation.onError((err, variables) => {
  Sentry.captureException(err, {
    extra: { eventId: variables.id, action: 'updateEvent' }
  });
});

Key Metrics Dashboard

Metric Alert Threshold
API error rate > 1%
Event fetch P95 latency > 500ms
Client JS error rate > 0.5% of sessions
Offline sync failure rate > 5%
View switch duration P90 > 200ms

Document Version: 1.0 · Last Updated: March 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment