DEV Community

Cover image for Frontend System Design Deep Dive1: Building a Web Chat Application
Vishwark
Vishwark

Posted on

Frontend System Design Deep Dive1: Building a Web Chat Application

Designing a modern, scalable, and real-time web chat application is no small feat. It demands a clear understanding of how browsers work, how to use the right APIs effectively, and how to make everything feel seamless for users. In this deep dive, we explore how to architect such a frontend system from scratch using the RADIO framework.


🧩 RADIO: A Frontend System Design Framework

RRequirements
AAPI Design
DData Modeling
IIntegration
OOptimizations

Let’s apply RADIO to our chat application frontend design.


🔷 R – Requirements: Defining the Web Chat Experience

Requirements help ensure you are solving the right problem and addressing the real needs of users and stakeholders.

✅ Functional Requirements

💬 Real-time Messaging: Users expect messages to appear immediately. This means the frontend must use real-time technologies like WebSocket to instantly reflect message delivery.

🕓 Chat History: When users open a chat, they should see previous messages. These should be loaded incrementally (paginated) so the browser isn't overwhelmed with large data sets.

✍️ Typing Indicators: Seeing when someone else is typing improves engagement. The client should detect input changes and send short-lived WebSocket messages to show this in the UI.

🔁 Infinite Scroll: Instead of loading all messages at once (which can crash the browser), load only what's needed as the user scrolls. Use libraries like react-window to improve performance.

📎 Media Uploads: Allow users to send files, images, or videos. Handle uploads using pre-signed URLs so files are sent directly to cloud storage, freeing your backend.

🔔 Push Notifications: Let users know when a new message arrives even if they aren’t on the chat screen. Implement using the Web Push API and service workers.

👤 Presence Status: Users should know who is online. This is handled through continuous updates over WebSockets and backed up by checking browser connectivity with navigator.onLine.

🧩 Multi-tab Sync: If a user has the app open in multiple tabs, any change (like a read receipt) should update everywhere. Use the BroadcastChannel API for communication between tabs.

✅ Non-Functional Requirements

⚡ Low Latency: Users should never feel lag. Use optimistic UI—update the interface before the server responds—and ensure frontend code executes efficiently.

📴 Offline Support: Enable offline reading and queued message sending. Store messages in IndexedDB and use service workers to handle offline logic.

📱 Mobile Optimization: Use responsive design to ensure usability across phones and tablets. Pay attention to touch events and screen performance.

♿ Accessibility: Follow ARIA guidelines and make sure the app works with screen readers and keyboards. This includes proper labeling, focus management, and contrast.

🔐 Security: Prevent common web vulnerabilities. Validate inputs, sanitize content, use HTTPS/WSS, and protect tokens.

📈 Scalability: Ensure the frontend handles large numbers of users and messages efficiently through proper rendering techniques and memory management.


🔷 A – API Design: REST + WebSocket Combo

A well-designed frontend depends on how it communicates with the backend. Use REST for traditional CRUD operations and WebSocket for live updates.

🔌 WebSocket for Real-Time Events

Establish a persistent WebSocket connection after login. Use it to:

  • Deliver new messages instantly
  • Notify when someone is typing
  • Broadcast online/offline status
  • Update message read receipts

Each event should have a type and payload field to make message handling predictable:

{
  type: 'NEW_MESSAGE',
  payload: { chatId: '123', message: 'Hello!' }
}
Enter fullscreen mode Exit fullscreen mode

Handle the socket connection globally using a context provider (WebSocketProvider) to ensure all components can access it without prop drilling.

🌐 REST APIs for Fallback and Fetching

Use REST APIs for:

  • Fetching chat lists and messages (e.g., with pagination)
  • Sending messages when WebSocket is unavailable
  • Getting upload URLs

REST APIs are stateless and reliable for historical or batch data. Libraries like React Query or SWR help manage these calls efficiently by caching, background updating, and error handling.


🔷 D – Data Modeling: Structuring Chat Data (with Code Examples)

Your application state must match how users interact with chat. Define clear data models to support UI rendering and interaction logic.

🧾 Message Model

Each message must have a unique ID, chat reference, sender, content, timestamp, and delivery status.

type Message = {
  id: string;             // Unique message identifier
  chatId: string;         // Reference to the chat the message belongs to
  senderId: string;       // ID of the user who sent the message
  content: string;        // The actual message content or media URL
  contentType: 'text' | 'image' | 'video' | 'file';
  timestamp: number;      // Unix timestamp (ms) of when the message was sent
  status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
};
Enter fullscreen mode Exit fullscreen mode

👥 Chat Model

Each chat contains users, messages, and metadata like unread count. Typing indicators can be stored in a separate key for real-time UI updates.

type Chat = {
  id: string;                 // Unique identifier for the chat
  participants: User[];       // List of users in the chat
  messages: Message[];        // Currently loaded messages
  unreadCount: number;        // Number of unread messages
  typingUserIds?: string[];   // IDs of users currently typing
};
Enter fullscreen mode Exit fullscreen mode


. Typing indicators can be stored in a separate key for real-time UI updates.

📦 Client-Side Storage with IndexedDB + Dexie

Use IndexedDB to persist data locally for offline access. Dexie.js offers a clean wrapper around IndexedDB.

Define database tables and types:

class ChatDatabase extends Dexie {
  messages!: Table<Message, string>;
  chats!: Table<Chat, string>;
  users!: Table<User, string>;
  pendingQueue!: Table<{ id?: number; type: 'SEND_MESSAGE'; payload: any; attempts: number; }, number>;

  constructor() {
    super("ChatAppDB");
    this.version(1).stores({
      messages: "++id, chatId, timestamp",
      chats: "++id",
      users: "++id",
      pendingQueue: "++id"
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Store messages, chat metadata, user profiles, and unsent actions in separate tables. This ensures you can:

  • Load old messages when offline
  • Show chat list quickly from local storage
  • Retry unsent messages later Use IndexedDB to persist data locally for offline access. Dexie.js offers a clean wrapper around IndexedDB.

Store messages, chat metadata, user profiles, and unsent actions in separate tables. This ensures you can:

  • Load old messages when offline
  • Show chat list quickly from local storage
  • Retry unsent messages later

🔷 I – Integration: Bringing It All Together

You now have models, APIs, and logic—time to stitch everything together.

⚙️ Core Tools in Harmony

  • React handles UI
  • Redux Toolkit/Zustand tracks global state
  • React Query/SWR manages server state and caching
  • Dexie.js persists messages and chats offline
  • Tailwind CSS quickly builds responsive UIs

🧱 Component Hierarchy Explained

Break the UI into parts:

  • ChatSidebar: list of chats
  • ChatWindow: current conversation
  • VirtualMessageList: messages with scroll loading
  • TypingIndicator: animated feedback when users type
  • FilePreview & MessageInput: controls for media and message sending

This modular structure supports independent updates, faster testing, and better performance.

🔄 State Management

Split state by scope:

  • Global State: socket connection, chat list, current user, active chat
  • Local State: message input, modals, scroll positions

Use a combination of context, Redux/Zustand, and local component state to ensure flexibility and performance.


🔷 O – Optimizations: Speed & Resilience

Now, let's enhance speed, reliability, and user experience. Instead of just listing techniques, we’ll explain where and why to use each.

🚀 Performance Techniques Explained

Virtualized Lists:
Use libraries like react-window or react-virtuoso for long message lists. These only render messages visible in the viewport, reducing DOM node count and improving scroll performance.

Lazy Load Media:
Images and videos shouldn’t be loaded until needed. Use the loading="lazy" attribute for <img> tags and Intersection Observer for custom components. This saves bandwidth and accelerates initial load.

Debounce Input Events:
Typing indicators shouldn’t be sent with every keystroke. Debounce the function that emits typing status to fire only after a delay (e.g., 300ms of inactivity). This minimizes network usage.

Memoization:
When a message component doesn't change, React should not re-render it. Use React.memo to wrap it, and useMemo or useCallback to memoize expensive calculations or handlers.

Code Splitting:
Use dynamic import() to load parts of the app (like settings or chat info) only when they’re needed. This keeps the initial bundle small and improves load time.

📶 Offline UX Enhancements in Practice

Service Worker Caching:
Cache static files (like HTML/CSS/JS) and avatars to load the app shell and user content even when offline.

Background Sync:
When a user sends a message offline, queue it. The service worker uses Background Sync to resend messages when the device reconnects.

Offline Indicators:
Detect connectivity with navigator.onLine and listen for online/offline events. Update the UI to show banners or disable send buttons.

🛡️ Security Practices That Matter

  • Always use secure connections (HTTPS, WSS).
  • Protect REST/WebSocket requests using JWT tokens.
  • Use DOMPurify to sanitize user content before rendering.
  • Check file type/size on client and server during upload.
  • Set CSP headers to restrict allowed sources of scripts and assets.

♿ Accessibility Done Right

  • Use proper ARIA roles like aria-live="polite" for live updates.
  • Ensure full keyboard navigation—Tab, Arrow keys, Esc to close.
  • Respect color contrast standards (WCAG AA).
  • Notify screen readers when new messages arrive or someone is typing.

✅ Final Thoughts

By applying the RADIO pattern, you bring structure and clarity to frontend system design:

  • Requirements ensure you're solving the right problems.
  • API Design establishes clean, consistent contracts between the frontend and backend.
  • Data Modeling builds a stable and predictable client state.
  • Integration ties together tools and services into a cohesive whole.
  • Optimization delivers performance, offline capability, and security.

Together, these steps lead to a frontend that is scalable, maintainable, and user-focused.

🙏 Thanks for reading!


Top comments (0)