Back to System Design
System Design
hard
mid

How would you design a calendar application like Google Calendar?

Day/week/month views with virtualized scrolling; events as positioned blocks computed by collision/layout algorithm; offline-capable via IndexedDB + sync queue; recurring events expanded client-side per visible range; drag-to-create and drag-to-move with optimistic updates; timezone-aware; keyboard + accessibility; real-time updates for shared calendars.

6 min read·~35 min to think through

Google Calendar is a deceptively rich system design — rendering, layout, recurrence, timezones, sync, and real-time all interact. The interview wants the big-picture architecture plus 2–3 deep dives.

1. Views

  • Day / Week / Month / Year / Schedule. Each is a different rendering of the same underlying event data.
  • The view determines the visible time range; that range drives data fetches and recurrence expansion.

2. Event layout — the hard part

In day/week view, overlapping events become side-by-side columns within a time slot. The algorithm:

  1. Sort events by start time.
  2. Walk through; group events that overlap into a cluster.
  3. Within a cluster, assign each event the leftmost column that's free; cluster width = max columns used.
  4. Render with absolute positioning: top = startMinutes * pixelsPerMinute, left = (columnIndex / numColumns) * 100%, width = (1 / numColumns) * 100%.
js
function layoutEvents(events) {
  events.sort((a, b) => a.start - b.start);
  const clusters = [];
  let cur = null;
  for (const e of events) {
    if (!cur || e.start >= cur.end) {
      cur = { events: [], end: e.end };
      clusters.push(cur);
    }
    cur.events.push(e);
    cur.end = Math.max(cur.end, e.end);
  }
  clusters.forEach(assignColumns);
  return events;
}

3. Recurring events

Don't store every occurrence. Store the rule (RRULE) and expand to occurrences per visible range on the client:

ts
event { id, title, dtstart, dtend, rrule: "FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=...", exdates: [...] }

For each fetch of "this week's events", expand recurrence rules across that range. Use a library like rrule.js. Exceptions and edits to a single occurrence are stored as overrides.

4. Timezones

  • Store event times in UTC with a timezone field (the calendar/event's display tz).
  • Convert on render based on user's display tz.
  • DST and travel are the gotchas — never naively add 24 hours; use date-fns-tz or Temporal.

5. Data layer & caching

  • React Query keyed by (calendar id, range) for visible-week fetches.
  • Prefetch adjacent ranges so navigation feels instant.
  • IndexedDB holds events for offline use (week or month at a time).
  • Sync queue in a service worker: mutations made offline are replayed when online.

6. Real-time

For shared calendars, push event changes via WebSocket (or SSE):

  • "event.created", "event.updated", "event.deleted".
  • Apply to local cache; reconcile if local has an optimistic edit.

7. Interactions

  • Click empty slot → create event modal at that time.
  • Drag empty area → create event with duration.
  • Drag existing event → move (optimistic; rollback on failure).
  • Resize handle → change duration.
  • Multi-day events span; render as a bar above the grid.

8. Rendering performance

  • Only render visible week / day events; outside is virtual / not in DOM.
  • Memoize event blocks; use transform: translate for drag to avoid layout.
  • requestAnimationFrame for drag handlers.

9. Accessibility

  • Grid semantics: role="grid" with rows for time slots, cells for slots.
  • Events as buttons with descriptive labels.
  • Keyboard nav: arrows to move focus, Enter to open, Shift+arrows to move the event.
  • Announce changes via a polite live region.

10. Auth & sharing

  • Per-calendar ACLs (owner / editor / viewer / free-busy).
  • Frontend conditional UI by role; server enforces.

Interview framing

"Views are projections of the same event data over a time range. The hardest rendering piece is the overlap layout algorithm: sort, cluster overlaps, assign columns, position with absolute coordinates and pixels-per-minute. Recurring events are stored as rules and expanded client-side per visible range using rrule.js, with exceptions as overrides. Times are UTC + tz on the wire; convert on render. Data flow: React Query keyed by (calendar, range), prefetch adjacent ranges, IndexedDB for offline, a service-worker sync queue. Shared-calendar real-time via WebSocket or SSE. Drag-to-move/resize is optimistic with rollback. Accessibility is a grid with keyboard nav. The depth questions usually go to overlap layout, recurrence expansion, and timezone correctness."

Follow-up questions

  • Walk through the overlap layout algorithm with an example.
  • Why expand recurrence client-side per range instead of storing occurrences?
  • How do you handle a recurring event with a single-occurrence edit?
  • Timezones — what goes wrong if you store local time?

Common mistakes

  • Storing local time without a timezone.
  • Materializing recurrence to rows on the server — explodes for infinite recurrences.
  • Re-layout on every drag frame.
  • Auto-jumping focus on real-time updates.

Performance considerations

  • Render only the visible range; memoize event blocks; use transform for drag; prefetch adjacent ranges; cache recurrence expansions.

Edge cases

  • DST transitions (23/25-hour days).
  • All-day vs timed events.
  • Cross-day events.
  • Recurring event end vs occurrence end.

Real-world examples

  • Google Calendar, Outlook Web, Fantastical, Cal.com.

Senior engineer discussion

Seniors get the overlap layout right, store recurrence as rules with overrides, treat timezones as a first-class concern, and architect offline + sync intentionally — not 'add a service worker later'.

Related questions