Frontend system design: 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.
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:
- Sort events by start time.
- Walk through; group events that overlap into a cluster.
- Within a cluster, assign each event the leftmost column that's free; cluster width = max columns used.
- Render with absolute positioning:
top = startMinutes * pixelsPerMinute,left = (columnIndex / numColumns) * 100%,width = (1 / numColumns) * 100%.
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:
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-tzorTemporal.
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: translatefor drag to avoid layout. requestAnimationFramefor 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.