System Design: Building a Simple Social Media Platform in Go

thesaltree

Saloni Agarwal

Posted on November 7, 2024

System Design: Building a Simple Social Media Platform in Go

In this article, we'll walkthrough the designing of a simplified social media platform using Go, focusing on low-level system design principles. Our platform includes core features like user registration, creating posts, handling likes and comments, and a notification system to keep users updated. This example illustrates how these features can be built into a system that’s modular, scalable, and efficient.

We'll use Go's concurrency capabilities and design patterns like the facade to create a streamlined and maintainable structure, allowing the platform to handle various user interactions seamlessly.

Key Components of Our Design

The social media platform we’re building focuses on these main features:

  • User Management: Registering and managing user profiles.
  • Post Creation and Interactions: Creating posts, liking, and commenting.
  • Notifications: Alerting users to relevant actions like likes and comments.
  • Concurrency: Efficiently handling simultaneous user actions.

Core Components of the System

Let’s break down the key components of our platform and see how each part integrates into the system.

  1. User Management

The UserManager component is responsible for user registration and profile management. Each user has essential profile details like ID, name, and bio, and the manager ensures users can be added and retrieved efficiently. Some key functions are:

type User struct {
    type User struct {
    ID             int
    Name           string
    Email          string
    Password       string
    DisplayPicture *string
    Bio            *string
    friends        map[int]*User
    posts          []*Post
}

type UserManager struct {
    users map[int]*User
    mu    sync.RWMutex
}

func (um *UserManager) AddUser(user *User) {
    um.mu.Lock()
    defer um.mu.Unlock()
    um.users[user.ID] = user
    fmt.Printf("User added: %d\n", user.ID)
}

func (um *UserManager) GetUserByID(userID int) (*User, error) {
    um.mu.RLock()
    defer um.mu.RUnlock()
    user, ok := um.users[userID]
    if !ok {
        return nil, fmt.Errorf("user not found")
    }
    return user, nil
}

func (um *UserManager) AddFriend(requesterID, receiverID int) error {
    requester, err := um.GetUserByID(requesterID)
    if err != nil {
        return err
    }

    receiver, err := um.GetUserByID(receiverID)
    if err != nil {
        return err
    }

    requester.AddFriend(receiver)
    receiver.AddFriend(requester)
    fmt.Printf("Friendship added between users: %d and %d\n", requesterID, receiverID)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

In a real-world application, the UserManager would connect to a database, but here we use a map for simplicity.

  1. Post Management

The PostManager handles user-generated content by managing posts, likes, and comments. This component allows users to create posts, like others’ posts, comment, and retrieve posts. Some key functions are:

type Post struct {
    ID              int
    UserID          int
    Content         string
    IsPublished     bool
    URLs            []*string
    Likes           int
    Comments        []*Comment
    PublishedAt     time.Time
    CommentsEnabled bool
    HiddenFromUsers map[int]bool
}

type PostManager struct {
    posts map[int]*Post
    mu    sync.RWMutex
}

func (pm *PostManager) GetPost(postID int) (*Post, error) {
    pm.mu.RLock()
    defer pm.mu.RUnlock()

    post, exists := pm.posts[postID]
    if !exists {
        return nil, fmt.Errorf("post not found")
    }
    return post, nil
}

func (pm *PostManager) AddPost(post *Post, user *User) {
    pm.mu.Lock()
    defer pm.mu.Unlock()
    pm.posts[post.ID] = post

    user.AddPost(post)
}

func (pm *PostManager) LikePost(postID int) (*Post, error) {
    pm.mu.Lock()
    post := pm.posts[postID]
    pm.mu.Unlock()

    if post == nil {
        return nil, fmt.Errorf("post not found")
    }

    pm.mu.Lock()
    defer pm.mu.Unlock()

    post.Like()
    return post, nil
}
Enter fullscreen mode Exit fullscreen mode

The PostManager could interact with a database to store and retrieve posts, allowing for filtering by various criteria.

  1. Notification Management

The NotificationManager is responsible for keeping users updated on platform activities, such as receiving a like or comment on their posts. Each notification type (like, comment, friend request) is sent through this manager, ensuring users are informed in real-time. Some key functions are:

type Notification struct {
    ID      string
    Type    NotificationType
    Content string
    UserID  int
}

type NotificationManager struct {
    notifications map[int][]*Notification
    mu            sync.RWMutex
}

func (nm *NotificationManager) AddNotification(userID int, notificationType NotificationType, message string) {
    nm.mu.Lock()
    defer nm.mu.Unlock()

    notification := NewNotification(fmt.Sprintf("notification-%d", time.Now().UnixMicro()), notificationType, message, userID)
    nm.notifications[userID] = append(nm.notifications[userID], notification)
}

func (nm *NotificationManager) GetNotificationsForUser(userID int) ([]*Notification, error) {
    nm.mu.RLock()
    defer nm.mu.RUnlock()

    notifications, ok := nm.notifications[userID]
    if !ok {
        return nil, fmt.Errorf("user not found")
    }
    return notifications, nil
}

Enter fullscreen mode Exit fullscreen mode

With NotificationManager, we can notify users of interactions related to their posts, allowing for a more engaging experience. In a production system, notifications could be sent via channels or push notifications.


Using the Facade Pattern with ActivityFacade

To simplify interactions between different components, we use the Facade pattern. ActivityFacade combines the functionalities of UserManager, PostManager, and NotificationManager, providing a unified interface for our social media app.

type ActivityFacade struct {
    userManager       *UserManager
    postManager       *PostManager
    notificationManager *NotificationManager
}

func (af *ActivityFacade) SendFriendRequest(requesterID, receiverID int) error {
    _, err := af.UserManager.GetUserByID(receiverID)
    if err != nil {
        return err
    }

    af.NotificationManager.AddNotification(receiverID, FriendRequestNotificationType, fmt.Sprintf("%d has sent you a friend request", requesterID))

    fmt.Printf("Friend request sent to user %d\n", receiverID)
    return nil
}

func (af *ActivityFacade) CommentPost(userID, postID int, content string) error {
    user, err := af.UserManager.GetUserByID(userID)
    if err != nil {
        return err
    }

    post, err := af.PostManager.CommentPost(user, postID, content)

    af.NotificationManager.AddNotification(post.UserID, CommentNotificationType, fmt.Sprintf("%d has commented on your post: %d", userID, post.ID))

    fmt.Printf("Comment added to post: %d\n", post.ID)
    return nil
}

func (af *ActivityFacade) HidePostFromUser(postID int, userID int) error {
    _, err := af.UserManager.GetUserByID(userID)
    if err != nil {
        return err
    }

    return af.PostManager.HidePostFromUser(postID, userID)
}
Enter fullscreen mode Exit fullscreen mode

With ActivityFacade, we can streamline user interactions with the platform, reducing the complexity of directly managing each subsystem. This approach makes the code more modular, maintainable, and easier to expand.


Handling Concurrency

In any social media platform, multiple users perform actions simultaneously. Go’s concurrency tools, particularly sync’s RWMutex, are ideal for handling concurrent reads and writes in a safe way.

Using RWMutex, we ensure that multiple users can read posts concurrently, but only one can like or comment at a time, preventing race conditions and data corruption.


Conclusion and next steps

Our low-level system design for a social media platform in Go provides a strong foundation for expanding features making it scalable and easy to maintain.

Potential areas for future enhancement include:

  • Real-time Notifications using WebSockets or push notifications.
  • Advanced Privacy Controls for friend requests and posts.
  • Persistent Data Storage with a database to replace the in-memory maps.

For full code implementation, please check the following repository:

GitHub logo thesaltree / low-level-design-golang

Low level system design solutions in Golang

Low-Level System Design in Go

Welcome to the Low-Level System Design in Go repository! This repository contains various low-level system design problems and their solutions implemented in Go. The primary aim is to demonstrate the design and architecture of systems through practical examples.

Table of Contents

Overview

Low-level system design involves understanding the core concepts of system architecture and designing scalable, maintainable, and efficient systems. This repository will try to cover solutions of various problems and scenarios using Go.

Parking Lot System

The first project in this repository is a Parking Lot System. This system simulates a parking lot where vehicles can be parked and unparked. It demonstrates:

  • Singleton design pattern for managing the parking lot instance.
  • Handling different types of vehicles (e.g., cars, trucks).
  • Parking space management across multiple floors.
  • Payment processing for…




💖 💪 🙅 🚩
thesaltree
Saloni Agarwal

Posted on November 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related