The Question
FE Design

Real-Time Notifications and Activity Feed System

Design a high-performance, real-time notification engine and activity feed for a modern collaborative web application. The design must address real-time ingestion patterns (such as SSE or WebSockets), client-side cache normalization, layout rendering optimizations (like windowing and layout thrashing prevention under peak loads), cross-tab synchronization of unread states, and comprehensive accessibility (WCAG) guidelines for dynamic alerts.
React
Tailwind CSS
TypeScript
IndexedDB
SSE
BroadcastChannel
Web Workers
Questions & Insights

Clarifying Questions

Q1: What is the expected volume of incoming notifications per client when active?
Assumption: Under normal usage, a client receives 1–5 events per minute. However, peak bursts can reach up to 60 events per minute. The system must batch updates to prevent UI thread blocking.
Q2: What real-time protocol is preferred or assumed to be available on the backend?
Assumption: We will use Server-Sent Events (SSE) because notifications are unidirectional (server-to-client). SSE is lightweight, operates over HTTP/2, and handles auto-reconnection out of the box.
Q3: Does the feed need to work offline, and should state persist across browser sessions?
Assumption: Yes, basic offline view is required for the last 50 notifications. We will persist notifications in IndexedDB for fast initial boot and offline access.
Q4: Do notifications support interactive, rich payloads (e.g., custom actions, inline forms, or dynamic text)?
Assumption: Yes. Notifications have standardized types (e.g., Mentions, Reactions, System Alerts) but carry structured payloads that require dynamic, polymorphic UI rendering.

Crash Strategy

Our strategy focuses on keeping the main thread free, maintaining visual stability, and optimizing network and memory overhead.
How do we ingest real-time events without triggering layout thrashing or constant re-renders?
Strategy: Introduce an Application-level "Ingestion Queue" that batches incoming events over a 300ms window before committing them to the client-side state cache.
How do we display thousands of feed items efficiently without killing browser memory?
Strategy: Implement a windowed/virtualized list for the activity feed so that only visible items exist in the DOM.
How do we maintain consistency of unread counts across multiple open tabs?
Strategy: Utilize a shared state synchronization channel (BroadcastChannel) to sync read states and badge counts across tabs instantly without hitting the server redundantly.
How do we ensure screen readers and keyboard users can navigate dynamic, real-time alerts gracefully?
Strategy: Implement ARIA-live regions for Toast notifications and follow APG standards for dropdown keyboard navigation.

Elite Bonus Points

Web Worker Offloading: Run the SSE connection and initial payload schema validation/normalization inside a Web Worker. This completely insulates the main thread from parsing raw network JSON chunks.
CSS Containment: Apply contain: content and content-visibility: auto to off-screen notification cards to optimize browser layout and paint calculation passes.
Tab Hibernation Control: Detect when the tab is in the background via the Page Visibility API, and dynamically throttle connection ping intervals or temporarily pause UI state commits, performing a single consolidated catch-up fetch when the tab becomes active again.
Design Breakdown

Design Summary

Concise Summary

An SSE-driven, client-cached architecture utilizing a Web Worker for ingestion and state normalization. It uses a virtualized feed layout with optimistic UI updates to achieve smooth performance and high responsiveness.

Major Components

App Shell: Coordinates the top-level layout, layout-level providers, and structural containers.
Header Layout: Top bar hosting navigation elements and the entry point for notifications.
Notification Panel Container: Dropdown overlay showing the latest unread notifications with action handlers.
Activity Feed Page: The primary route containing category filters and the infinite scrolling layout.
Notification Toast Manager: Dynamic toast queue managing life cycle, entry/exit animations, and ARIA alerts.
Feed List Filter: Component managing tabbed categories (e.g., All, Mentions).
Notification Card: Polymorphic leaf component representing a single feed/notification item.
Toast Item: Transient leaf component displaying real-time alert UI.

CUJ Walkthrough

Receive Real-Time Notification:
An SSE event triggers on the server -> Web Worker captures the event, parses/validates payload, and posts it to the Ingestion Queue -> Queue groups events for 300ms -> Store updates state -> Notification Toast Manager renders a transient Toast Item inside an ARIA-live region -> Header Layout unread badge increments instantly.
Infinite Scroll Feed Navigation:
User scrolls down the Activity Feed Page -> IntersectionObserver triggers API requests via Infrastructure clients -> Data normalized and appended to the Cache -> Virtualized list mounts visible Notification Card items dynamically, recycling DOM nodes.
Mark as Read Interaction:
User clicks "Mark as Read" on a Notification Card -> Store performs optimistic UI update (clears badge count and dims card) -> Background REST mutation dispatching -> If failed, state rolls back gracefully with an error toast message.

Simplicity Audit

We reject heavy dual-direction protocols (WebSockets) in favor of Server-Sent Events (SSE) because client-to-server notifications are entirely transactional actions easily handled by simple REST endpoints. We also avoid heavy state libraries in favor of TanStack Query + local state queueing, which significantly decreases bundle size and engineering complexity.

Architecture Decision Rationale

Why this architecture is the best?: It decouples connection ingestion from rendering. By using a Web Worker and batched local states, the UI never drops below 60 FPS even during heavy notification cascades.
Requirement Satisfaction: It guarantees low latency toasts, satisfies memory caps using virtualization, and preserves tab bandwidth utilizing Page Visibility controls.

System Diagram

Architecture Deep Dive

Presentation Layer

Component Hierarchy

The layout flows from the top-down:
App Shell: Bootstraps the app context, state providers, and base layout grids.
Header Layout: Pinpoints the navigation strip. Holds the notification badge element.
Notification Panel Container: Contained within the Header as a dynamic absolute overlay popup. Renders a list of the 5 most recent Notification Cards.
Activity Feed Page: Triggered by routing. Houses the Feed List Filter and a virtual scroll container rendering active Notification Cards.
Notification Toast Manager: Mounted globally at the App Shell level. Spawns transient Toast Item cards overlaying the right-hand view grid.

Interaction Layer

Component Design: Notification Cards are polymorphic. Based on the type domain model (e.g., mention, system, like), the card dynamically swaps icons, metadata fields, and action buttons.
Event Handling: All actions (clicks, swipe-to-dismiss on mobile, dismiss buttons) are managed through standard callback bubbles, debounced in the component to prevent double-click mutations.
Accessibility:
Toasts use role="log" and aria-live="polite" so screen readers speak new alerts without interrupting active typing.
The dropdown uses ARIA-expanded states and focus management; hitting Escape closes the dropdown and returns focus to the trigger badge.
Input Validation: Action items (e.g., reply inputs in mentions) are validated client-side against schema constraints before transmission.
Animation and transitions: CSS transitions using transform: translate3d() and opacity are utilized to ensure Hardware Acceleration (GPU rasterization) during card insertion and deletion.

Rendering Layer

CSR vs SSR: Hybrid-approach. SSR delivers the shell layout and static skeleton feeds for zero CLS (Cumulative Layout Shift). CSR hydrates the connection, boots SSE, and handles dynamic state updates.
Lazy Loading: Code splits the main Activity Feed Page using dynamic imports to keep initial load bundles small.
Virtualization: Uses row height measurement strategies inside the virtualizer to map indexes to CSS translation transforms, allowing the DOM to retain exactly only N + 4 visible card entries.
Heavy UI: CSS properties such as will-change: transform are applied to cards during transition cycles to limit recalculation.

UI Frameworks / Tools

Component logic is built using React.
Styling is implemented using Tailwind CSS for low-weight atomic classes.
Virtualization utilizes @tanstack/react-virtual to dynamic-measure variable row heights.
---

Application Layer

Data Fetching Layer

Fetch Patterns: SSE is used for stream ingestion. Standard HTTP REST endpoints are used for mutations (e.g., POST /notifications/read-all).
Parallel Requests: When loading the activity feed, the page pre-fetches both the category meta-counts and the virtualized paginated items list in parallel.
Pagination: Implemented as cursor-based pagination to avoid duplication errors when real-time items are inserted at index 0.
Offline Support: IndexedDB acts as a physical write-through cache. New notifications fetched via SSE or API are mirrored instantly to IndexedDB.

State Management Layer

Global State: Managed using TanStack Query for cache invalidation, combined with a lightweight React Context for the connection lifecycle status (connected, offline, connecting).
Ingestion Queue: To prevent layout thrashing from continuous SSE updates, a buffer array stores incoming payloads. Every 300ms, the queue flushes, pushing a single consolidated transaction batch to the state store.
Tab Synchronization: Instantiated using a BroadcastChannel instance named "notification_channel". When Tab A marks notification ID_99 as read, it sends a message via the channel, triggering Tab B to dynamically mark ID_99 as read in its local cache state without requesting the backend.

Routing & Navigation

SPA Routing: Handles transitions from Toast clicks directly to nested detail routes.
Route Guards: Evaluates JWT expiration status prior to establishing the SSE handshake.
---

Domain Layer

                        ┌────────────────────────┐
                        │      Client Cache      │
                        └───────────▲────────────┘
                                    │
                         Hydrate / Normalization
                                    │
┌──────────────────┐    ┌───────────┴────────────┐    ┌────────────────────┐
│                  │───►│   Validation Engine    │───►│ Notification Event │
│   Raw JSON Payload│    │   (Type Safeguard)     │    │   (Domain Model)   │
│                  │    └────────────────────────┘    └────────────────────┘
└──────────────────┘

Business Rules & Use Cases

Decoupled from the React tree. Defines validation rules for notification types.
Evaluates event eligibility: decides if an incoming SSE payload should trigger a visible Toast (e.g., if the user is currently looking at the sender's active chat window, suppress toast and mark as read automatically).

Entities & Models

NotificationEvent: Core domain model enforcing immutable values:
  interface NotificationEvent {
    readonly id: string;
    readonly type: 'MENTION' | 'SYSTEM' | 'REACTION';
    readonly sender: { readonly name: string; readonly avatar: string };
    readonly timestamp: number; // UTC Epoch
    readonly isRead: boolean;
    readonly payload: Record<string, unknown>;
  }
Type Guards: Functions like isMentionNotification(event) mapping payloads into specific view schemas safely.

Inter-layer Contracts

The domain layer defines abstract adapters for validation and tracking metrics. This isolates application logic from framework updates.
---

Infrastructure Layer

API / Network

SSE Client: Implements standard EventSource wrappers. Includes custom heartbeat tracking: if no heartbeat ping is received from the server within 45 seconds, the client closes the socket and triggers auto-reconnection.
Reconnection Strategy: Exponential backoff with jitter to protect infrastructure from a "thundering herd" scenario when reconnection storms occur.
Request Deduplication: Uses abort signals (AbortController) to automatically cancel outdated API pagination requests when filtering tabs rapidly.

Storage

IndexedDB Wrapper: Uses a thin wrapper (e.g., idb-keyval or native wrapper) to store static data up to a 50MB quota limit.
SessionStorage: Used to transiently track user dismissal of specific system-wide sticky alerts during the session duration.
---
Wrap Up

Wrap-up

Trade-offs & Evaluation

SSE vs. WebSockets: We chose Server-Sent Events (SSE). While WebSockets support two-way communication, they require custom framing protocol management, have poor fallback integrations over standard proxies, and require complex server architecture. Since our client mutations are simple HTTP requests, SSE is cleaner, supports HTTP/2 multiplexing natively, and handles network dropping seamlessly.
DOM Virtualization vs. CSS content-visibility: While content-visibility: auto is simple, it does not clean up memory (the DOM elements still exist). In active feeds with thousands of dynamic elements, actual DOM size degrades browser performance. We combined structural Virtualization (physical node limits) with contain: paint on mounted nodes to deliver optimal UI scrolling.

Recovery Actions

Network Degradation: If network latency degrades, Toast animations degrade to simple css-toggles (disabling heavy JS springs) to conserve processing cycles.
Buffer Backpressure: If the Ingestion Queue size exceeds 50 items during peak traffic bursts, the system temporarily disables real-time toast presentation and alerts the user with a single summary alert banner: "You have 50 new notifications". This protects the UI from UI lockups.
---