How to Build a Chat App / WhatsApp Clone (Technologies, Methods and Architecture)

How to Build a Chat App / WhatsApp Clone (Technologies, Methods and Architecture)

Context:

In this blog I will be sharing standard procedures, my learnings and experiences while I was building a Chat App. Instead of traditional method of implementing websocket connection, I will be using realtime-update service provided by firestore on a blackbox called onSnapshot().

My Definition of Chat App:

A Chat App is an application which enables mutual connections and let users share messages(text +multimedia). It requires a low latency architecture so that the recipients may get the messages from the sender, instantly.

What Stack I used and Why?:

By keeping above point ("low latency arch") in mind, I have chosen the below technologies:

  1. Firestore (Major part of the App) - A Google Cloud Provider which offers cloud storage access for messaging, storage, and many more different kinds of services. My App only required "messaging service" for instant message sharing with snapshots, and "storage service" to store the multimedia files (profile images + chat specific files).

  2. React - I have chosen this framework because of my experience and expertise on it.

  3. TailwindCSS - The reason to not choose to write Vanilla CSS is that I find inline-CSS very convenient and easy, than creating separate .css files and redundantly writing those long-lengthed words.

  4. TypeScript - Needless to say, Custom types are more suitable for large and scalable applications.

Features which I have planned to include:

  • Unique Chat room for every pair of users. (End-to-End)

  • Unique Chat room for a selected set of users. (Groups)

  • Sharing Image, Video, and any other File.

  • Realtime Messaging with Status Indicators:
    - Waiting
    - Sent
    - Seen

  • Last-Updated-Time and Last-Message on every Chat Opening Bar.

  • Usage of Queue Data Structure to Synchronize Messages and avoid Race Condition.

  • Ordering the Chats (Groups and Profiles) based on Last-Updated-Time.

  • New-unseen-message Indicator. (Notification)

  • Responsive Design - Split Pages View for Large (and) Single Page View for Small Screen Sizes.

The Architecture behind real-time communication:

  • Creating Groups and Users is easy. The main users and groups list screen will be listening to the two different database collections.

  • Thankfully Firestore provides onSnapshot() listener for real-time communication with database. Else we may had to go with manual implementation of websocket connection. This is the major benefit of using Firestore DB, rather than some high latency DBs which are not specifically build for messaging services, such as MongoDB.

  • This is how onSnapshot() listener works:
    - It is written under the useEffect hook.
    - According to my research I have found that onSnapshot() too implements a websocket connection between the client and DB.

    - On the initial render, it gets the current/latest list of documents from the specifies collection on DB.

    - when a document or a field on document(s) changes, or a new document is added, the snapshot listener get the new updated list (all) of documents and it re-executes the provided callback.

      useEffect(()=>{
          const q = query(collection(DB_instance, collection_name);
          const unSub = onSnapshot(q, myCallBack);
          return ()=>{
              unSub();
          }
      }, []}
      const myCallBack = (snapshot)=>{
          // use "snapshot" object to access the latest documents and thier fields
      }
    

    The Architecture behind Synchronization of Messages and Race-Condition Avoidance:

  • Since the messages must be on "Waiting" state if a previous message is currently being sent, or if the sender is in offline mode.

  • Hence we have to maintain a local list of messages on client side in which:

    - New Messages to be sent are pushed with status as "Waiting".

    - Statuses of Previous Messages are updated.

    - New Messages from recipient are pushed.

  • The standard approach for ensuring synchronization is by using Queue Data Structure at the client side to transmit the messages one after the other, according to the time factor, so that the app would work even when the user is offline.

  • Whenever the user clicks on send button, first enqueue the message, then push the message into local list, and then trigger the message sending function(add doc) until the queue is empty.

      const [list, setList] = useState([]);
    
      const sendMessageHandler = (newMessage)=>{
          queue.enqueue(newMessage);
          setList(l=>{
              return [...l, newMessage];
          });
      }
    
      useEffect(()=>{
          const sendMessage = async ()=>{
              const newMessage = queue.dequeue();
              await setDoc(doc_ref, newMessage);
              if(!queue.isEmpty()){
                  sendMessage();
              }
          }
          if(!queue.isEmpty()){
              sendMessage();
          }
      }, [list]);
    
  • The message docs are rendered on the chat screen in chronological order, which ensures synchronization even when user(s) is offline.

  • As of Race-Condition, I have not encountered a problem upto yet, but one should ensure it. And to do so, the approach that I used in my previous similar project (FlashChat) is by using a canAccess boolean field on the chat room schema. This is a slow approach. Hence, we should use an efficient approach for robust/scalable app.

Thank You for reading ❤️ !
Subscribe/Follow for more content like this.
Twitter | LinkedIn