Building a Music Streaming Service with Python, Golang, and React: From System Design to Coding Part 2

koladev

Mangabo Kolawole

Posted on August 6, 2024

Building a Music Streaming Service with Python, Golang, and React: From System Design to Coding Part 2

Streaming is an interesting topic in software engineering. Whether it is about music, video, or just simple data, applying this concept from a design, architecture, and coding perspective can become quickly complex if not thought through correctly.

In this article series, we will build a service to stream music using Golang and React. We will start by showing the incremental system design and the corresponding code implementation for each step. By the end of this series of articles, you will learn:

  • How to build an API using Python and Golang

  • How to serve HTTP range requests

  • How to create a system design/architecture for a music streaming service

In the last part of this series, we have built the first version of the application, where a user can retrieve a list of songs from the browser and play selected music from the browser.

We have noticed some cons to our architecture mostly concerning caching and the addition of a more reliable and distributed file storage system.

In this article, we will focus on adding a caching and CDN component to the current architecture.

If you are interested in more content covering topics like this, subscribe to my newsletter for regular updates on software programming, architecture, and tech-related insights.

Setup

You can clone the v1 for the project using the following instructions:

git clone -b v1 https://github.com/koladev32/golang-react-music-streaming.git
cd golang-react-music-streaming
make setup
Enter fullscreen mode Exit fullscreen mode

This will set up the project by cloning the wanted branch and installing packages and dependencies.

Once the project is set up, we can now move to discussing architectures.

Architecture

In the recent project, here is the architecture of the project.

This architecture is a straightforward monolith that consolidates all backend functionality—such as storage, song management, and database connections—into a single server or domain. However, during periods of high demand for retrieving the song list via the API, this setup can lead to slower response times or even database downtime.

Additionally, since the storage is managed directly on the server, it faces the challenge of handling numerous requests for the same files, which can significantly strain bandwidth and degrade overall application performance. To address these issues, we need to adopt a more distributed and scalable storage solution, like a CDN.

To improve the system, we will redesign the architecture by incorporating a caching component and transitioning to a more efficient storage solution.

Adding caching and modifying the storage

In the updated architecture, we've introduced a new component called CACHE on the server. This component interacts with the API to handle data saving and retrieval. Additionally, we’ve moved the storage functionality off the server, indicating that storage is now managed as a separate service rather than being part of the server itself.

For caching, various solutions are available, such as Redis and Memcached. In this article, we will focus on implementing Redis. For storage, I will provide configuration steps for using AWS S3 and CloudFront. While I can’t offer configurations for other cloud providers, I will list equivalent tools available from other popular cloud services.

Let's start by adding the caching method.

Adding Redis Caching

Redis is an in-memory data storage tool that can be used as a database or a caching tool. To install Redis according to your OS, following the instructions stated in the documentation.

To make sure the Redis server is running, run the following command.

redis-server start
Enter fullscreen mode Exit fullscreen mode

The Redis server should be running at localhost on port 6379. The next step will be installing the django-redis package in the backend environment.

pip install django-redis
Enter fullscreen mode Exit fullscreen mode

Now in the settings.py file of the backend project, add the following lines for configurations.

# settings.py

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        },
    }
}

CACHE_MIDDLEWARE_SECONDS = 300  # 5 minutes
Enter fullscreen mode Exit fullscreen mode

In the code above, we are defining the CACHES settings and setting the default cache backend on Redis. The LOCATION parameter is the Redis address to the server running on the machine.

We are also configuring a setting called CACHE_MIDDLEWARE_SECONDS which indicates the number of seconds before the cache invalidates the data. In our case, we defined it to 300 seconds. However, this value should be equal to the average of times a mutation operation (CREATE, UPDATE, DELETE) is done on your application.

The next step is to rewrite the SongViewSet to add our caching logic.

from django.core.cache import cache
from rest_framework import viewsets, permissions, status
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from rest_framework.response import Response

from music.models import Song
from music.serializers import SongSerializer


class SongViewSet(viewsets.ModelViewSet):
    queryset = Song.objects.all()
    serializer_class = SongSerializer
    permission_classes = [permissions.AllowAny]

    cache_key = 'song_list_cache'

    @method_decorator(cache_page(60 * 5))  # 5 minutes
    def list(self, request, *args, **kwargs):
        return super(SongViewSet, self).list(request, *args, **kwargs)

    def create(self, request, *args, **kwargs):
        response = super(SongViewSet, self).create(request, *args, **kwargs)
        if response.status_code == status.HTTP_201_CREATED:
            # Invalidate cache on create
            cache.delete(self.cache_key)
        return response

    def update(self, request, *args, **kwargs):
        response = super(SongViewSet, self).update(request, *args, **kwargs)
        if response.status_code == status.HTTP_200_OK:
            # Invalidate cache on update
            cache.delete(self.cache_key)
        return response

    def destroy(self, request, *args, **kwargs):
        response = super(SongViewSet, self).destroy(request, *args, **kwargs)
        if response.status_code == status.HTTP_204_NO_CONTENT:
            # Invalidate cache on delete
            cache.delete(self.cache_key)
        return response
Enter fullscreen mode Exit fullscreen mode

In the code above, we extend the different actions on the viewsets, list, create, update, and destroy. On the SongViewSet, we are defining the cache_key attribute. This is the key that is used by to invalidate cache on create, update, and delete methods.

With this done, we have now added Redis configuration for this application. In the next section, we will integrate S3 and Cloudfront into the codebase.

Upgrading the Storage: Integrating AWS S3 and CloudFront

In the previous section, we focused on adding caching with Redis to our application. In this section, we will upgrade our storage solution by integrating AWS S3 and CloudFront. This will provide a more scalable, distributed, and efficient way to manage and serve media files.

Why S3 and CloudFront?

  • Amazon S3 (Simple Storage Service): S3 is a scalable object storage service that allows you to store and retrieve any amount of data at any time. It is ideal for storing static files like images, audio files, and documents due to its durability, scalability, and easy accessibility.

  • Amazon CloudFront: CloudFront is a content delivery network (CDN) that caches and delivers content from edge locations around the world. By using CloudFront, you can improve the performance and speed of delivering content to users by reducing latency and offloading traffic from your main server.

Integrating S3 with Django

To integrate S3 into your Django project, follow these steps:

  1. Install Required Packages

    First, you need to install the boto3 and django-storages packages, which provide the necessary tools for interacting with S3 from Django.

    pip install boto3 django-storages
    
  2. Configure Django to Use S3

    Update your settings.py file to configure Django to use S3 as the default file storage backend. You will need to provide your AWS credentials and S3 bucket information.

    # settings.py
    
    INSTALLED_APPS = [
        # other installed apps
        'storages',
    ]
    
    # AWS S3 settings
    AWS_ACCESS_KEY_ID = 'your-aws-access-key-id'
    AWS_SECRET_ACCESS_KEY = 'your-aws-secret-access-key'
    AWS_STORAGE_BUCKET_NAME = 'your-s3-bucket-name'
    AWS_S3_REGION_NAME = 'your-s3-region'  # e.g., 'us-west-1'
    AWS_S3_FILE_OVERWRITE = False
    AWS_DEFAULT_ACL = None
    
    # Static files settings
    STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
    STATIC_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/static/'
    
    # Media files settings
    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
    MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/media/'
    

    Ensure to replace the placeholders with your actual AWS credentials and bucket details.

  3. Update Your S3 Bucket Policy

    Ensure your S3 bucket policy allows public access for static files and sets the appropriate permissions for media files. You can adjust these settings in the AWS S3 console under the "Permissions" tab of your bucket.

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "Stmt1405592139000",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": [
                    "arn:aws:s3:::bucketname/*",
                    "arn:aws:s3:::bucketname"
                ]
            }
        ]
    }
    

With AWS S3 setup, we can now move to the setup of CloudFront.

Setting Up CloudFront

Once you have integrated S3, the next step is to set up CloudFront to serve your files.

  1. Create a CloudFront Distribution
* Go to the [AWS CloudFront Console](https://console.aws.amazon.com/cloudfront/).

* Click on **"Create Distribution"**.

* Choose **"Web"** and configure the following settings:

    * **Origin Domain Name**: Select your S3 bucket.

    * **Origin Path**: Leave empty.

    * **Viewer Protocol Policy**: Set to **"Redirect HTTP to HTTPS"** for better security.

    * **Allowed HTTP Methods**: Choose **"GET, HEAD"** for static files.

    * **Cache Based on Selected Request Headers**: Choose **"All"** if you need to handle different versions of files.
Enter fullscreen mode Exit fullscreen mode
  1. Configure Distribution Settings
* **Distribution Settings**: Set your preferred settings, such as enabling caching, setting the default root object, and configuring SSL certificates if needed.
Enter fullscreen mode Exit fullscreen mode
  1. Update Django Settings for CloudFront

    After creating the CloudFront distribution, update your settings.py file to use the CloudFront URL for serving media and static files.

    # settings.py
    
    # Static files settings
    STATIC_URL = f'https://your-cloudfront-url.cloudfront.net/static/'
    
    # Media files settings
    MEDIA_URL = f'https://your-cloudfront-url.cloudfront.net/media/'
    

    Replace your-cloudfront-url with the domain name of your CloudFront distribution.

If we follow these instructions, we will have integrated AWS S3 and CloudFront, thus enhancing the scalability and performance of the music streaming service.

With our new scalable architecture in place, we now face a challenge: users can access direct download links and potentially bypass the streaming service, undermining the purpose of streaming content through our application.

One potential solution could be to use WebSockets to stream music. This approach involves breaking the song file into small chunks and buffering them through WebSockets. While effective, WebSockets are primarily designed for real-time communication and may be an over-engineered solution for this use case.

Instead, we'll employ HTTP range requests, which are more suited for efficient streaming without the complexity of WebSockets. In the next article, we'll introduce a Golang-based service that utilizes HTTP range requests to stream music effectively.

Stay tuned!🚀

Conclusion

In this article, we enhanced our music streaming service by integrating Redis for caching and leveraging AWS S3 and CloudFront for scalable, distributed storage. These changes address the challenges of performance and scalability, ensuring a smoother user experience and more efficient content delivery.

You can find the codebase for this article on this branch https://github.com/koladev32/golang-react-music-streaming/tree/v1.5.

Next, we'll focus on implementing a Golang-based service that utilizes HTTP range requests for streaming music effectively. This approach will optimize streaming performance while avoiding the complexity of WebSockets.

Stay tuned for the next article! 🚀

If you enjoyed this article and want to stay updated with more content, subscribe to my newsletter. I send out a weekly or bi-weekly digest of articles, tips, and exclusive content that you won't want to miss 🚀

💖 💪 🙅 🚩
koladev
Mangabo Kolawole

Posted on August 6, 2024

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

Sign up to receive the latest update from our blog.

Related