System design
Design a Notification System
Difficulty: 🔴 Hard · Est. time:
1h· Tags:#realtime#websocket#state#a11y
Asked at: Meta, LinkedIn, Uber, Atlassian · Related: Chat · News Feed · Interview Patterns
1. The Question
Design a frontend notification system (bell icon + dropdown + toasts): real-time in-app notifications, an unread badge, a notification center with history, and transient toasts for live events.
2. Requirements
Functional
Non-functional
3. High-Level Design
┌────────────────┐ WebSocket/SSE ┌──────────┐
│ Notification │ ◀─────────────── │ Server │ push new events
│ Store │ REST (history) └──────────┘
│ - list (norm.) │ ────────────────▶ GET /notifications?cursor
│ - unreadCount │
└───────┬────────┘
├─▶ Bell + Badge (subscribes to unreadCount)
├─▶ Inbox dropdown (subscribes to list, paginated)
└─▶ Toast manager (subscribes to high-priority stream)
- Transport: SSE or WebSocket for push; REST for paginated history + mark-read mutations.
- Store: normalized notifications keyed by id + a derived
unreadCount. - Three consumers: badge, inbox, and toast manager — all read the same store.
4. Deep Dives & Trade-offs
Push transport: SSE vs WebSocket → notifications are mostly server→client, so SSE is a great fit (simpler, auto-reconnect, works over HTTP). Use WebSocket if you already have one for chat, or need client→server on the same channel.
Delivery guarantees → on connect/reconnect, fetch "since last seen" so you never miss events during downtime. Dedupe by id (reconnect may replay). Keep a lastSeenId/cursor.
Unread badge accuracy → the badge is derived state (count of unread), not a separate counter you increment — that avoids drift. Server sends authoritative counts periodically to reconcile.
Toasts vs inbox → not every notification toasts (that's noisy). Only high-priority/real-time-relevant ones toast; all land in the inbox. Toast manager = a queue with max visible, auto-dismiss timers, pause-on-hover, and stacking.
Optimistic mark-as-read → update UI immediately, send mutation, roll back on failure. "Mark all read" is a single batched request.
Pagination & memory → inbox loads pages on scroll (cursor-based); old notifications aren't all held in memory.
Accessibility → the toast region is an aria-live="polite" (or assertive for urgent) region so screen readers announce new notifications. The bell button exposes the unread count via aria-label ("Notifications, 3 unread"). Inbox is keyboard-navigable; Esc closes.
Cross-tab sync → use BroadcastChannel or a storage event so marking read in one tab updates the badge in others.
5. What Interviewers Probe
- SSE vs WebSocket vs polling for notifications.
- How the unread badge stays accurate (derived, reconciled).
- Missed events on reconnect (fetch-since + dedupe).
- Toast queue management (max visible, timers, pause-on-hover).
- Optimistic mark-as-read + rollback.
- Accessibility (live regions, badge labels).
- Cross-tab consistency.
6. Curated Resources
- MDN: Server-Sent Events ⭐ — the push transport
- MDN: ARIA live regions ⭐ — accessible toasts
- MDN: BroadcastChannel — cross-tab sync
- web.dev: notifications — web push (background)