Design a Notification System UI
Frontend system design for notifications — inbox, toasts, push permission, realtime, badges, and preferences.
advanced4 min read
- system-design
- interview
- architecture
- notifications
Scope the problem
In scope (frontend):
- In-app notification inbox
- Unread badge
- Toast/snackbar for ephemeral events
- Preference center (channels/categories)
- Web Push opt-in UX (not full push infra)
- Realtime delivery of new items
Out of scope: email/SMS provider internals, APNs/FCM scaling deep dive (interfaces only).
Requirements
| Type | Examples |
|---|---|
| Functional | list, mark read, mark all, deep link targets |
| Non-functional | low-latency badge; don’t drop events when offline briefly |
| Product | CTR to target; opt-out rates; notification fatigue |
Surfaces
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Toast │ │ Bell inbox │ │ System push │
│ ephemeral │ │ persistent │ │ OS surface │
└──────────────┘ └─────────────┘ └──────────────┘
overlapping but different retention
Rule of thumb:
- Toast: immediate feedback on user action or high-urgency in-session event
- Inbox: durable history + audit
- Push: user away from tab; requires permission
High-level architecture
┌─────────────────────────────────────────────────────┐
│ NotificationProvider │
│ - unreadCount │
│ - items cache (infinite query) │
│ - realtime subscription │
├──────────────┬──────────────────┬───────────────────┤
│ Bell UI │ Toast bridge │ Preferences page │
└──────────────┴──────────────────┴───────────────────┘
│
▼
REST/GraphQL + WebSocket/SSE
Data model (client)
type NotificationItem = {
id: string;
category: "social" | "billing" | "system" | "marketing";
title: string;
body: string;
createdAt: string;
readAt: string | null;
href: string; // deep link
actor?: { name: string; avatarUrl: string };
};
type NotificationsPage = {
items: NotificationItem[];
nextCursor: string | null;
unreadCount: number;
};
API sketch:
GET /notifications?cursor=POST /notifications/:id/readPOST /notifications/read-allGET/PUT /notification-preferences- Realtime:
notification.createdevent
State & caching
| Concern | Approach |
|---|---|
| List | infinite query, newest first |
| Unread badge | server count + optimistic decrement |
| Mark read | optimistic; reconcile on error |
| Realtime insert | prepend if at top; else bump badge only |
| Dedupe | by notification id |
// on websocket message
queryClient.setQueryData(["notifications"], (old) => prependDedupe(old, item));
queryClient.setQueryData(["unread"], (c) => (c ?? 0) + 1);
When inbox open and item visible, auto-mark-read (debounced) is a product choice — call it out.
Realtime
- WebSocket or SSE for badge + inbox
- Fallback polling every N minutes when socket down
- Resume: fetch
since=lastEventIdon reconnect to fill gaps
Push permission UX
- Never cold-prompt on first paint
- Contextual prime (“Get alerted when build finishes”) → then browser prompt
- Store permission state; guide to browser settings if blocked
- Service worker shows notification; click focuses client route
Preferences
type Prefs = {
channels: { inApp: boolean; push: boolean; email: boolean };
categories: Record<string, { inApp: boolean; push: boolean; email: boolean }>;
};
UI: matrix of category × channel. Marketing defaults off in many jurisdictions — product/legal input.
Accessibility & UX
- Badge has accessible text (
aria-label="3 unread notifications") - Inbox list semantics; mark-read buttons named
- Toasts: polite vs assertive by severity (Toast system)
- Don’t flood toasts for every inbox item — collapse (“3 new notifications”)
Performance
- Virtualize long inbox
- Avatars lazy
- Socket messages coalesce in rAF if bursty
- Code-split preferences page
Tradeoffs
- Single store for toast+inbox vs separate — shared event bus, different retention
- Auto read-on-view vs explicit — accuracy vs unread utility
- Push vs email for critical billing — redundancy
- Client filter vs server fanout of preferences
Failure modes
- Clock skew on ordering
- Duplicate delivery — idempotent ids
- Permission denied — degrade gracefully
- Deep link to deleted resource — targeted 404 state
Interview close
Separate surfaces (toast/inbox/push) → data model + optimistic read → realtime with catch-up → preference matrix → permission priming. Mention fatigue and a11y badge labeling.
Related on this site
- Toast Notification System
- Realtime WebSocket UI Design
- PWA Install and Offline Shell
- System Design Interview Framework