logo
Challenges of Building Real-Time Collaborative Systems
by John Oba - Afrodev26 July, 2024 • 6 min read
Challenges of Building Real-Time Collaborative Systems

Imagine using an app where messages appear instantly, or collaborating on a document where changes sync in real-time. That's the magic of real-time applications, and as developers, we're tasked with creating these seamless experiences beyond the normal GET, POST request lifecycle.

My journey with real-time systems started with a mix of curiosity and apprehension. At first, concepts like WebSockets seemed far fetched - almost like trying to understand a foreign language. But as I started experimenting, that initial fear transformed into excitement.

Building these systems is like constructing a high-speed railway. It's not just about getting from A to B; it's about doing it smoothly, quickly, and reliably. My manager often reminds me, "Think of the apps you use daily - they just work, right? That's our benchmark." As tech leads, our goal is to create products that users don't even have to think about - they simply work, every time.

I've been exploring tools like Socket.IO, WebRTC, and gRPC. At first, they seemed as complex as trying to understand quantum physics! But as I've worked with them, I've realized they're more like puzzles. Challenging? Absolutely. But also incredibly rewarding when you see everything click into place.

These systems are what power the seamless experiences we've come to expect in modern apps. Whether it's a multiplayer game updating in real-time or a video call connecting people across the globe, real-time communication is at the heart of it all.

Also, from an engineering perspective, it is a top tier feature to implement, whether it's Server-to-Client or Client-to-Client communication.

Some challenges I encountered when crafting web-based real-time collaborative systems include but are not limited to the following:

Maintaining & Scaling a Socket System

For a Socket.IO-based server that heavily relies on socket connections for different tasks, one scaling solution I utilized was adding namespaces to my different socket instances. This approach helped organize and manage the connections more effectively.

// Server-side (Node.js with Socket.IO)
const io = require('socket.io')(server);

// Create namespaces
const chatNamespace = io.of('/chat');
const notificationsNamespace = io.of('/notifications');

chatNamespace.on('connection', (socket) => {
    console.log('A user connected to the chat namespace');
    // Handle chat events
});

notificationsNamespace.on('connection', (socket) => {
    console.log('A user connected to the notifications namespace');
    // Handle notification events
});

Dealing with Component rerendering

For a React-based client, managing socket connections while dealing with component re-renders was a herculean task. The frequent re-renders posed challenges in maintaining stable and consistent connections. Following best practices was crucial and which includes not instanciating a socket instance from a useEffect hook.

// Bad practice
const [socket, setSocket] = useState(null);

useEffect(() => {
    const socket = io("https://example.com");
    setSocket(socket);
}, []);

Here's a better approach:

// Good practice

// socket.js
const socket = io("https://example.com");
export default socket;

// component
import socket from "./socket";
import { useEffect } from "react";

useEffect(() => {
    socket.on("connect", () => {
        console.log("Connected to the server");
    });
}, []);

For detailed information, visit Socket.IO React Guide.

Ensuring Security and Privacy

Real-time systems must also address security and privacy concerns, such as preventing unauthorized access. Implementing authentication mechanisms like JWT (JSON Web Tokens) can enhance security.

// socket.js
const socket = io("https://example.com", {
    auth: {
        token: clientToken
    }
});

on the server side, this can be handled by using the socket.handshake.auth object to get the token and verify it depending on the authentication strategy.

Also, on the client side, the token may not be reachable from the socket.js file, so you can instantiate the socket first, but set autoConnect as false, then use the socket.auth object to pass the token to the socket.

// socket.js
import { io } from "socket.io-client";

const socket = io("https://example.com", {
    autoConnect: false
});

export default socket;

// component
import socket from "./socket";

socket.auth = {
    token: clientToken
};

socket.connect();

Implementing a reconnection strategy

Even with the best connection strategy, there are times when the connection is lost. Implementing a reconnection strategy is crucial to ensure that the connection is restored as soon as possible. One approach is to use the socket.io-client library to handle the reconnection process.


// socket.js
import { io } from "socket.io-client";

const socket = io("https://example.com", {
    autoConnect: false,
    reconnection: true,
    reconnectionAttempts: 5,
    reconnectionDelay: 1000,
    reconnectionDelayMax: 5000,

});

const connectSocket = (s, token) => {
    s.auth = {
        token: token
    };
    s.connect();
}

export { connectSocket };
export default socket;

// component
import socket, { connectSocket } from "./socket";

connectSocket(socket, clientToken);

export default socket;

Often times the socket is not connected when the component is mounted, so you can use the useEffect hook to check if the socket is connected and make a retry

useEffect(() => {
    if(!socket.connected){
        connectSocket(socket, clientToken);
    }
}, []);

You can also use a recursive retry strategy to ensure that the socket is connected before subscribing to events.

Real-time communication systems are like the nervous system of modern apps. They're what make your chat messages appear instantly or your collaborative document update as your colleague types.

While the technical details can be complex, the key is to start with the basics and build up. Understanding how to use real-time communication is the crucial first step, much like learning to drive before you can build a car. With the right tools and strategies, creating robust and scalable real-time applications becomes an achievable goal.


More Stories from Afrodev

2023 AfroDev