JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeQuestionssystem-design
PrevNext

Learn the concept

Real-Time System Architecture

system-design
mid
real-time

Design a real-time notification system for a web application

real-time
notifications
sse
server-sent-events
websocket
push-notifications
event-source
state-management
system-design
Quick Answer

A real-time notification system uses Server-Sent Events (SSE) or WebSockets for delivery, a priority queue for ordering, toast UI for immediate alerts, a notification center for history, and the Notifications API for browser-level alerts when the tab is not focused.

Detailed Explanation

Real-time notifications are a critical feature in modern web applications. Designing one well requires decisions about transport mechanism, state management, UI patterns, offline handling, and browser permissions.

Requirements

Functional Requirements:

  • Toast notifications for immediate, in-app alerts
  • Notification center (bell icon with dropdown) showing history
  • Unread count badge
  • Mark as read (individual and bulk)
  • Notification persistence across page navigations
  • Browser push notifications when the tab is not focused
  • Notification grouping (e.g., "3 people liked your post")

Non-Functional Requirements:

  • Sub-second delivery latency
  • Graceful degradation on network failure
  • No duplicate notifications
  • Ordered by priority, then timestamp
  • Support 10,000+ concurrent connections per server

Transport Mechanism: SSE vs WebSocket vs Polling

| Feature | SSE | WebSocket | Long Polling | |---------|-----|-----------|-------------| | Direction | Server → Client only | Bidirectional | Server → Client | | Protocol | HTTP/2 | WS (separate) | HTTP | | Auto-reconnect | Built-in | Manual | Manual | | Complexity | Low | Medium | Low | | Best for | Notifications, feeds | Chat, gaming | Legacy fallback |

SSE (Server-Sent Events) is the best choice for notifications because:

  • Notifications are one-directional (server to client)
  • SSE has built-in reconnection with Last-Event-ID for resuming missed events
  • Works over standard HTTP/2 with multiplexing (no extra connection overhead)
  • Simpler to implement than WebSockets, works through proxies and firewalls
  • Natively supported in all modern browsers via EventSource

Use WebSockets only if you also need client-to-server real-time communication (e.g., chat, collaborative editing).

Reconnection Strategy

SSE handles reconnection automatically, but you should enhance it:

  1. EventSource reconnects automatically with exponential backoff
  2. Send Last-Event-ID header on reconnect so the server can replay missed events
  3. Each event has a unique, monotonically increasing ID
  4. Server maintains a short buffer (last 5 minutes) of events for replay
  5. If disconnected longer than the buffer window, do a full fetch from the REST API

Notification Priority & Grouping

Not all notifications are equal. Implement a priority system:

  • Urgent (priority 1): Security alerts, payment failures — show immediately as toast, play sound
  • High (priority 2): Direct messages, mentions — show as toast
  • Medium (priority 3): Comments, reactions — show badge count, queue in notification center
  • Low (priority 4): System updates, tips — only show in notification center

Grouping reduces notification fatigue:

  • Group by type + target: "3 people liked your post" instead of 3 separate notifications
  • Use a time window (e.g., 5 minutes) for grouping
  • Show the latest actor name: "Alice and 2 others liked your post"

State Management

Notification state includes:

{
  notifications: Notification[]     // All notifications
  unreadCount: number               // Badge count
  toastQueue: Notification[]        // Active toasts
  connectionStatus: 'connected' | 'reconnecting' | 'disconnected'
}

Key decisions:

  • Use a sorted array (by priority, then timestamp) rather than appending
  • Limit stored notifications (e.g., keep last 100)
  • Sync unread count with server on reconnection to handle missed mark-as-read events
  • Persist notification center state to localStorage so it survives page refreshes

Offline Queueing

When the user goes offline:

  1. Detect via navigator.onLine and the offline/online events
  2. Show a subtle "offline" indicator in the notification center
  3. On reconnect, SSE's Last-Event-ID replays missed events
  4. If the gap is too large, fetch from the REST API: GET /api/notifications?since={lastEventId}
  5. Deduplicate by notification ID before adding to state

Browser Notifications API

For when the user's tab is not focused:

  1. Request permission: Notification.requestPermission()
  2. Only request on user interaction (not on page load) to avoid auto-denial
  3. Use document.visibilityState === 'hidden' to decide whether to show browser notification
  4. Include action buttons for quick responses
  5. Handle click to focus the tab and navigate to the relevant content

Toast UI Pattern

  • Stack toasts vertically (bottom-right on desktop, top on mobile)
  • Auto-dismiss after 5 seconds for non-urgent, persist for urgent
  • Maximum 3 visible toasts; queue the rest
  • Support dismiss by swipe (mobile) or close button
  • Animate entrance and exit for polish

Error Handling & Edge Cases

  • Duplicate notifications: Deduplicate by notification ID client-side
  • Tab sync: Use BroadcastChannel API to sync read status across tabs
  • Memory leaks: Clean up EventSource on component unmount
  • Stale tokens: Handle 401 responses by refreshing auth and reconnecting

Code Examples

EventSource (SSE) connection with auto-reconnection and event replayTypeScript
type NotificationPriority = 'urgent' | 'high' | 'medium' | 'low';

interface AppNotification {
  id: string;
  type: string;
  title: string;
  body: string;
  priority: NotificationPriority;
  timestamp: number;
  read: boolean;
  groupKey?: string;
}

type ConnectionStatus = 'connected' | 'reconnecting' | 'disconnected';
type StatusCallback = (status: ConnectionStatus) => void;
type NotificationCallback = (notification: AppNotification) => void;

export function createNotificationStream(
  onNotification: NotificationCallback,
  onStatusChange: StatusCallback
) {
  let eventSource: EventSource | null = null;
  let lastEventId: string | null = null;

  function connect() {
    // Build URL with last event ID for replay
    const url = lastEventId
      ? `/api/notifications/stream?lastEventId=${lastEventId}`
      : '/api/notifications/stream';

    eventSource = new EventSource(url);

    eventSource.onopen = () => {
      onStatusChange('connected');
    };

    // Listen for notification events
    eventSource.addEventListener('notification', (event) => {
      lastEventId = event.lastEventId;
      const notification: AppNotification = JSON.parse(event.data);
      onNotification(notification);

      // Show browser notification if tab is hidden
      if (
        document.visibilityState === 'hidden' &&
        (notification.priority === 'urgent' ||
          notification.priority === 'high')
      ) {
        showBrowserNotification(notification);
      }
    });

    // SSE auto-reconnects on error
    eventSource.onerror = () => {
      onStatusChange('reconnecting');
      // EventSource handles reconnection automatically
      // with exponential backoff
    };
  }

  function showBrowserNotification(notification: AppNotification) {
    if (Notification.permission === 'granted') {
      const n = new Notification(notification.title, {
        body: notification.body,
        tag: notification.id, // Prevents duplicates
        requireInteraction: notification.priority === 'urgent',
      });
      n.onclick = () => {
        window.focus();
        n.close();
      };
    }
  }

  function disconnect() {
    eventSource?.close();
    eventSource = null;
    onStatusChange('disconnected');
  }

  function requestBrowserPermission(): Promise<NotificationPermission> {
    return Notification.requestPermission();
  }

  // Start connection
  connect();

  // Reconnect when coming back online
  window.addEventListener('online', () => {
    if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
      connect();
    }
  });

  return { disconnect, requestBrowserPermission };
}

Real-World Applications

Use Cases

Social Media Engagement

Facebook, Twitter/X, and LinkedIn use real-time notifications for likes, comments, shares, and follows. They group notifications (e.g., '5 people liked your post') and prioritize direct interactions over passive updates.

E-commerce Order Tracking

Amazon and Shopify send real-time order status updates (confirmed, shipped, delivered) via in-app notifications and browser push, keeping customers informed without requiring them to check their email.

DevOps Alerting Dashboards

Grafana and PagerDuty use real-time notification systems to alert on-call engineers about incidents with priority-based routing and escalation policies.

Mini Projects

Notification Center with Toast Stack

intermediate

Build a notification center component with a bell icon showing unread count, a dropdown with notification history, and a toast stack for new alerts. Include mark-as-read, mark-all-read, and auto-dismiss functionality.

Real-Time Activity Feed

advanced

Create a collaborative activity feed that shows real-time updates from multiple users using SSE. Include reconnection handling, offline indicators, and event replay on reconnect.

Industry Examples

Slack

Uses WebSockets for its real-time messaging and notification system, with sophisticated presence detection and do-not-disturb scheduling to prevent notification fatigue

GitHub

Uses SSE for real-time notification delivery of PR reviews, issue mentions, and CI/CD status updates, with a notification center that supports filtering by repository and type

Figma

Combines WebSockets for real-time collaboration with notification events for comments and file updates, using multiplayer cursors and presence indicators

Resources

MDN - EventSource (Server-Sent Events)

docs

MDN - Notifications API

docs

MDN - Server-Sent Events Guide

docs

Related Questions

What is the difference between client state and server state?

junior
data-layer

What are the trade-offs between client-side and server-side rendering?

junior
rendering
Previous
Design an autocomplete/typeahead search component
Next
How do you design a component library / design system for multiple teams?
PrevNext