Building a Music Streaming Service with Python, Golang, and React: From System Design to Coding Part 2
Mangabo Kolawole
Posted on August 6, 2024
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
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.
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
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
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
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
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:
-
Install Required Packages
First, you need to install the
boto3
anddjango-storages
packages, which provide the necessary tools for interacting with S3 from Django.
pip install boto3 django-storages
-
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.
-
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.
- 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.
- Configure Distribution Settings
* **Distribution Settings**: Set your preferred settings, such as enabling caching, setting the default root object, and configuring SSL certificates if needed.
-
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 🚀
Posted on August 6, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.