How I approached a system design problem as an intern
As a software engineer intern, I had to design a system with limited resources and no prior knowledge. This post describes how I approached the problem and created a workable solution.
You may find something of value or have a laugh because of stupid decisions. Anyway, keep reading!
We wish to create a service where the user would enter some details (like their height, weight, etc.) and upload a picture of their face. Using this, we plan to generate a 3D avatar that conforms to those details and looks like them.
- We have a React client where the form to fill details is present.
- We have a Node server where all the requests are sent by the client.
- We have some Python code on a remote server that generates the avatar.
Here’s the simple solution to give you the flow of the application.
- The user submits their details on the React application in their browser. This data is sent to the Node server in multipart format.
- The server receives the data and starts to upload the image to the S3 bucket.
- A MongoDB object is created using the remaining details.
- The URL of the uploaded image is added to the object which is then saved into the database.
- The server sends this document to the Python code. For this, we’ll have to turn this code into a server and expose an API.
- The code generates the avatar, uploads it into a different bucket, and returns its URL back to the Node server.
- The Node server saves this URL in a new field in the existing document.
Constraints and challenges
The above solution works for a simple case. These complexities force us to devise a different solution —
The Python code can only serve one request at a time.
The avatar generation may take up to 1 minute. We want to keep the user posted about the status of their request.
We want to send the user a notification when the generation is completed.
We don’t expect a lot of traffic on the website so our design considerations are made accordingly.
Solution to Problem 1: Queue data structure
(We did not consider multithreading due to some specific reasons but it is a possible solution)
We often see in algorithmic problems that when we are given constraints, we start looking towards using data structures. They can help us organize our data so it fits our use.
Here, the data was the request to generate an avatar. As we can serve only one request at a time and we wish to serve on a first-come-first-serve (FCFS) basis, a queue data structure seemed to be the right choice for this.
So, I searched about how we can add requests to a queue which eventually brought me to the topic of message queues. I’ve compiled some of my learnings in this article.
As we were already using AWS, we decided to use Simple Queue Service (SQS) for our purpose.
The solution would be modified as shown below.
We will add a queue in between the Node server and the Python code. All requests from the Node server will be pushed into the queue which solves the following issues —
- The Node server doesn’t have to worry about sending requests one at a time. It will just push all the requests that it has to the queue.
- The Python code gets to work on a single task at a time.
- The CPU running the Python code will not stay idle as it would be working on a job all the time. When it finishes the current job, it will poll the queue for the next one.
- The Python code now doesn’t have to be a server. It can just use the SQS API to receive its next task. Instead of the task being pushed to it by the Node server, it will pull the task from the queue.
Here is a good article about push-pull architecture.
Solution to Problems 2 and 3: Continuous polling
To send a message from the server to the client is the opposite of the standard request-response model. Thus, we have the following options instead of it —
- Websockets — a two-way connection between client and server
- Server-sent events — similar to a publisher-subscriber model
- Continuous polling/short polling — keep sending requests to the server to check for updates
- Long polling — send polling request once and maintain the connection until something updates
I might go into details of each of these approaches in a future post. But as of now, we chose continuous polling for our small-scale app.
The React code was written such that every 10 seconds, it asks the server if the avatar has been generated. To keep track of this, a new field called “status” is applied to the MongoDB document.
The status is changed as the document goes into different stages and this is returned to the client on status requests. Now, when the status is “completed”, the client knows it and can take action accordingly.
The user can now know the status of their avatar generation process and they will also be notified when it’s completed. Thus, we have solved the last two problems.
We used message queues to fulfill one request at a time requirement along with FCFS priority. We got other benefits like maximum CPU utilization too.
We were able to keep the client posted about their avatar generation process by continuously pinging the server.
So that was an intern’s take on a small-scale system design. What are your thoughts about it?
Connect with me on Linkedin.