Nowadays, using Docker with Microservices is common practice, as it brings isolation, portability, scalability, and more. It will be a surprising for a backend engineer / full-stack engineer not to familiar with Docker (of course, combined with Kubernetes). In the article, I will introduce the core concept of Docker from the practical perspective, whether you are a college student who are interested in leraning Docker or a junior software developer just beginning your career in the software industry, you should be able to grasp the essentils of Docker that you need in practice in 10 minutes by reading the article.

The workflow with Docker in industrial projects

I found the following chart that perfectly explain the workflow with Docker in practice Docker Tutorial for Beginners [FULL COURSE in 3 Hours].

Trulli
Fig.1 - The Workflow with Docker

As shown above, the steps of the workflow are (all Docker related terms will be explained in the next section):

  • After you complete the application development and commit the code to Git, Jenkins will be notified via webhooks
  • Jenkins will buid the docker images according to the dockerfile provide
  • Jenkins will push the built images to the Docker repository, like Docker Hub or AWS ECR (Elastic Container Registry)
  • The server will pull the images from the Docker repository, then create and run containers according to the Docker Compose files (*compose.yaml files)

The Core Concepts of Docker

In the above workflow, I mentioned many essetianls in Docker. In this section, I will explain them in details.

Docker Sever (Docker Deamon)

It’s responsible for managing Docker containers, images, networks, and volumes. It is run on the same machine where Docker is installed. The Docker Client will send all requests to Docker Server.

Docker Client (CLI)

The Docker Client is the interface that users interact with when using Docker.

Docker Hub

Docker Hub is a Docker Repository. It is the default public registry for Docker images, created and maintained by Docker, Inc.

Docker Container

An instance of an image is called a container. It is the running image.

Docker Image

Stopped container. Docker image are read-only layers, and the docker container is the writable layer on top of a image.

Docker Compose

Docker Compose is a powerful tool for defining and managing multi-container Docker applicaitons. In practice, you can sepcify your applicaton’s services, networks, and volumes in a docker-compose.yml file, and then use a single command to start
them all.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3.9'

services:
mongodb:
image: mongo:6.0 # Replace with the desired MongoDB version
container_name: mongodb-container
restart: always
ports:
- '27017:27017' # Expose MongoDB's default port
environment:
MONGO_INITDB_ROOT_USERNAME: admin # MongoDB root username
MONGO_INITDB_ROOT_PASSWORD: password # MongoDB root password
volumes:
- mongodb_data:/data/db # Persistent data storage

volumes:
mongodb_data:
driver: local

Dockerfile

A Dockerfile is a script that contains a series of instructions to build a Docker image, inccluding the base image,
dependencies, configurations, and commands to run the application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Use the official Node.js LTS image as the base image
FROM node:18-alpine

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json files to the container
COPY package*.json ./

# Install dependencies
RUN npm install --production

# Copy the rest of the application files
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Define the command to run the application
CMD ["npm", "start"]

About CMD ["npm", "start"]: Specifies the command to start the Node.js application. Make sure your package.json has a start script defined (e.g., “start”: “node index.js”).

Private Docker Registry

Normally, we push our Docker images to pricate docker registry rather than a public one. A commonly used private Docker registry is AWS ERC (Elastic Container Registry).

Docker Volumes

Docker Volumes is used to persist data in Docker and prevent data loss in stateful applications. They allow folders from the host’ physical file system (e.g. /home/mount/data) to be mounted into the virtual file system (e.g. /var/lib/mysql/data) of Docker.

There are three types of Docker volumes:

  • Docker-managed volumes: Managed entirely by Docker and stored in its default location.
  • Anonymous volumes: Automatically created without a specific name, typically for temporary use.
  • Named volumes: Explicitly named and can be referenced by their names. Named volumes are the most common choice in production environments due to their reusability and ease of management.

Summary

The above covers the core concepts of Docker, providing a quick start for beginners. You can explore each part in greater details based on your interests.

Comment and share

There are tons of articles online that offer tutorials about how to use Promise.all. I will not reiterate those. In this article, I aim to brief summarise the use of Promise.all in real-world projects for those programmers transitioning from junior to mid-level.

What do we need to keep in mind when considering using Promise.all

  • Order matters. Promise.all returns the results in the same order as the source.
  • Fail one, Fail all. If one of the promise rejects, then the Promise.all will not wait for the remaining promise to execute but rejects right away.

When do we need Promise.all

In real project, normally when we have an array of data, for each element in the array, we need to do some async/await operations, this is where we need Promise.all. For example, in the following code snippets from a real project, we have an array called idsForDelete, for each id in this array, we need to call an async method removeMonitorTask(apiKey, id). Therefore, we use Promise.all to do async/await calls on all ids in the array.

1
2
3
4
5
6
7
8
...
const deleteIds = await Promise.all(
idsForDelete?.map(async (id) => {
const response = await removeMonitorTask(apiKey, id);
return response?.data[0]?.trackerId?.toString() === id.toString();
})
...
);

How do we handle exceptions with Promise.all

There are two ways of handling exceptions with Promise.all:

  • catch an exception at the end of Promise.all;
  • catch an exception in each individual promise.

Only catching exceptions in the latter way when we need to:

  • Keep the results of other promise
  • Want to know the reason for each rejected promise

Check the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p1 = Promise.resolve(10);
p2 = Promise.resolve(20);
p3 = new Promise(function (resolve, reject) {
setTimeout(reject, 100, 'test');
}).catch((err) => {
console.log(`internal err: ${err}`);
});

(async () => {
const res = await Promise.all([p2, p3, p1]);
console.log(res);
})().catch((err) => {
console.error(`err: ${err}`);
});

Execution result:

1
2
3
-- internal err: test
-- [ 20, undefined, 10 ]

As we can see from the execution result that even p3 is rejected, we still get the results of previous p1 and the p2 afterwards.

What if we only catch the exception after Promise.all?

1
2
3
4
5
6
7
8
9
10
11
12
p1 = Promise.resolve(10);
p2 = Promise.resolve(20);
p3 = new Promise(function (resolve, reject) {
setTimeout(reject, 100, 'test');
});

(async () => {
const res = await Promise.all([p2, p3, p1]);
console.log(res);
})().catch((err) => {
console.error(`err: ${err}`);
});

Result:

1
-- err: test

As shown above, Promise.all rejects all, we are not able to see the result of p2.

That is it, that is all you need to know about Promise.all in most of the time when developing real-world projects.

Comment and share

Last week I was working on a task of adding multiple language support to the React Native app that is under developing. I used the package i18next as the backhone to support the feature. React Redux is also used to store the language choice as one of the app settings. During the development, I encountered an unexpected bug: The home screen does not re-render once I changed the language setting. It took me around 2.5 hours to resolve it, hence, I consider it worth a blog to write the think process and the solution down.

Context of the Issue

First, let us go through the context of the issue.

Store the Language Settings in React Redux

In the i18next, it has a global variable i18n.language to store language choice and is applied to the whole app. However, it is not enough, if we do not store the language setting, once the app is closed, the user choice will lost. Therefore, I used React Redux to store the language setting, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
const initialState = {
language: '',
}

export const appSettingSlice = createSlice({
name: 'appSettings',
initialState,
reducers: {
getLanguageSetting: (state) => {
return state.language
},
setLanguageSetting: (state, action) => {
if (!action.payload) return
state.language = action.payload
},
resetLanguageSetting: (state) => {
state.language = ''
},
},
})
...

The Expected Scenario

The project is structured as blow. The language in redux is set under set-language/index.jsx, along with i18n.language. And set-language is routed from my-account. What I expected is that if I change the language, once I switch back to the home screen, it should re-render and present with the changed language.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- root
-- app
-- (tabs)
-- home
...
-- index.jsx
...
-- my-account
...
-- set-language
-- index.jsx
-- index.style.js
...
-- index.js // entry
-- _layout.js
...
...
-- index.js
...

The Re-render Issue

Things did no go as I expected. Once I change the language setting and switch back to the home screen, it changes nothing, the scren still show the preivous language.

RCA (Root Cause Analysis)

I added a useEffct hook into (tabs)/home/index.jsx, and would like to see why the home screen does not re-render. The code is shown below:

1
2
3
4
5
useEffect(() => {
console.log(`language: ${language}`);
console.log(`i18n.language: ${i18n.language}`);
i18next.changeLanguage(i18n.language);
}, []);

Then I changed the language and i18n.language from the set-language again, I found out that as soon as I changed the variables, the logs were shown on the terminal (means that the useEffect hook in the home screen was triggered). When I switched back to the home screen, the terminal did not print out the logs again (means that useEffect hook was not trigered).

Hence, I reached to the conclusion that under the tabs navigation of Expo Router, the tabs are somehow binded together, i.e. change states in one tab, will cause the useEffect hooks in the other tabs triggered. In this case, the other tabs consider themselves are re-rendered already. For example, in my case, I changed the language state under my-account/set-language, the useEffect hook in the home screen is triggerd, since I am still in the set-language screen, the components in the home screen will not be re-endered. When I switch back to the home screen, as there is no state change, the useEffect hook will not be triggered, and the language in the home screen stay with the old one.

The Solution

After identifying the root cause, the thought for the solution is obvious: find a method to ‘force’ the (tabs)/home screen to be re-rendered when change the language state in the (tabs)/my-account/set-language screen.

Enlightened by the information provided from Device context doesn’t work with Expo Router, I use the i18n.language as the key value for the Stack component under app/_layout.js (which is the parent componet for (tabs) ). Therefore, everytime the i18n.language is changed, the key value of the Stack component of app/_layout.js will be changed, thus cause all tabs under the (tabs) directory be re-rendered. The source code is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...

const Layout = () => {
const { i18n } = useTranslation()

return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Stack key={JSON.stringify(i18n?.language)}>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
</Stack>
</PersistGate>
</Provider>
)
}
...

Compile and run the app again. Bingo! The feature works as expcted :)

Comment and share

In the last few months, I am working on a Defi app which is based on the Expo platform. One of the ‘must have’ thing is that the app need to store insensitive user data locally, such as app settings. After comparing with several potential solutions, like react-native-mmkv (Not compatible with Expo), expo-sqlite (Not compatiblw with the Expo for web), redux-persist (build on top of Async Storage, compatible with Android, iOS and web, easy to use), I decided to adopt Redux.

I searched a bit regarding integrating Redux into Expo, however, in my Expo project, I used tabs navigation structure, so there is no app.js as the main entry (The file structure is listed blow). And all the solutions that I found only works on the Expo project that has an App component (normally app.js) as an entry point. After a few hours of trouble shooting, I finally found the solution myself and decided to share the method in this article, hopefully it will help someone in the future.

1
2
3
4
5
6
7
8
9
10
11
-- root
-- app
-- (tabs)
-- home
...
-- index.js // entry
-- _layout.js
...
...
-- index.js
...

Step by Step Guide

Although the way to the destination is rugged, the solution is quite simple. To embed Redux into an Expo project with the tabs navigation structure, we only need to add the redux provider into two files: app/index.js and app/_lauout.js.

app/index.js

As shown below, we wrap the <Redirect href="/home" /> with the Redux provider and the PersistGate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRootNavigationState, Redirect } from 'expo-router';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
// store and persistor are self-defined common redux component, nothing special
import { store, persistor } from '../services/redux/store';

const Home = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Redirect href="/home" />
</PersistGate>
</Provider>
);
};

export default Home;

app/_layout.js

This is the tricky part, I could not image that to make Redux work, I need to wrap the Stack with the Redux provider and the PersistGate until I found out that this is a key step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Stack } from 'expo-router';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
import { store, persistor } from '../services/redux/store';

const Layout = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</PersistGate>
</Provider>
);
};

export default Layout;

After adding the two, I am able to use the Redux in the files under /app. Happy coding: )

Comment and share

Overengineering is a word that a software engineer will hear about from time to time through his/her whole career. I recalled a design case that I encountered a few months back that perfectly explain what is overengineering in practice.

The Definition of Overengineering

On Wikipedia, Overengineering is defined as “the act of designing a product or providing a solution to a problem in an elaborate or complicated manner, where a simpler solution can be demonstrated to exist with the same efficiency and effectiveness as that of the original design.” I would like to simplify it as: choose a more complicated solution to resolve a problem when there exist simpler ones.

How to avoid overengineering

The tricky thing is, in practice, normally it is hard to identify overengineering in the system that is designed by yourself.

From my own personal experience, except harness yourself with rich knowledge and hands on experiences, the best option is to leverage collective wisdom, i.e. consult as many people as you can.

Case Study

A few months ago, my team had a design decision to make for a web system that is under developing. The scenario can be simplified as:

  • A group of users, on peak time, the QPS was estimated as around 5K, would request and compete for a limited number of resources (the number of the resources would be less than 500) in a pool.
  • Users would request the resources via RestAPI call. The users’ requests would be served in First Come First Serve style, non pre-emptive.
  • The individual resource itself has a rate limiting, if it receives more than 20 requests per minute, then it will return an error and become unavailable for a few minutes.
  • VIP users could be assigned a resource exclusively and still under the “20 requests per minute” rule.
  • According to internal tests (from the ones who were not from the dev team), the possibility that requests from the user might take more than 3 seconds to finish was high (roughly more then 98%).

The design is targeted for resolving two issues of the scenarios:

Resources competition.

How should the system handle the case that multiple users competing for the limited number of resources?

A single resource received more then 20 requests per minute.

How should the system handle the situation that a resource receives more then 20 requests per minute from a VIP user?

One of my colleague instantly proposed using message queues as the core of the solution. To be specific:

for “Resources Competition”

  • All the user requests should be put into a message queue and be pulled out in FIFO style.
  • A worker would pick a request and assign a resource once there is any available ones in the pool.

for “More than 20 Requests issue”

  • Assign a message queue for each resource, store the requests sent from the user into a message queue, a worker pull requests from the queue in FIFO style.
  • Only let the requests be pulled out from the queue under the rate of 20 per minute.

The solution is shown below.

Trulli
Fig.1 - MQ Solution

Add graph

Everything seems perfectly “reasonable” right now. However, in the evening of the same day, I gave the problem a second thought, then I realised that message queues might be an overengineering solution.

Resources Competition

From User Experiences Perspective
  • Resources Competition. On the one hand, let us assume if a message queue is not used here, then once all the resources are assigned, a user will receive an error for the resource request and the user will be informed by a message that he/she should retry after a few minutes. On the other hand, if a message queue is adopted here and all the resources are assigned, then a user’s request will store in the queue and the user will need to wait for the response for unforeseeable time, which does not improve user experiences compare to the first case.
From System Performance Perspective

Well, it is obvious that introducing message queue will not improve system performance.

Therefore, for “Resources Competition”, adopting a message queue is an overengineering option.

More than 20 Requests issue

I was thinking about forcing a user to wait for 3 seconds as an alternative option, i.e. each time the user consumes the resource and takes less than 3 seconds (60 seconds / 20 = 3), then force the user to wait until 3 seconds passed (3 seconds count from taking the ownership of the resource to release it), it ensures that the user will not trigger the 20 requests per minutes error.

From User Experiences Perspective

Consider the fact that more then 98% of chance that a user’s request will take more than 3 seconds to complete, it means that forcing a user to wait for 3 seconds passed if a request takes less than 3 seconds will not significantly impact user experiences.

From System Performance Perspective

One resource per message queue add heavy burden on the backend system while it does not improve the system performance in any means compare to the forcing wait method.

Therefore, for “More than 20 Requests issue”, using message queue solution is an overengineering option too.

With the arguments and conclusions in mind, the other day, I successfully convince the team to avoid adopting message queue which relief the backend dev team :).

Comment and share

Recently, I helped some small business (less then 20 people) integrating payment module into their systems. I consider that it worths sharing the experiences, as this also benefit the cases include: selling code on Github, selling online courses on personal websites etc.

The payment platform manages everything for you

There are some payment platforms that manage everything for you. You do not need to bother to design the frontend UIs of “placing order” page (choosing the products listed on the page) and “paying” page (filling credit card information and actually transferring the money), not to mention the backend logics behind the UIs. The platforms do all these things for you. Let us take lemon squeezy as an example(Well, I swear I did not receive ads fee from lemon squeezy, but maybe they should do so :D).

For integrating lemon squeezy into a system, all a software engineer needs to do is:

  • Set a store page on lemon squeezy, fill the page with information includes: product information and payment information, as shown below. The customers should complete all the steps regarding buying products on the store page: select products, filling payment information and transferring money.

    Trulli
    Fig.1 - An example of lemon squeezy store page
  • Add a link to the frontend page where you would like to redirect the customers to the store page of lemon squeezy

  • Add a page for customers to activate the keys recieved from lemon squeezy. After a customer complete payment on the store page, lemon squeezy will return an activation key to the customer, the customer should use the key to claim the ownership of the product on the website. On the backend of the website, when activating the key, the software engineer needs to do two things: 1) validate the key with lemon squeezy; 2) activate the key with lemon squeezy.

With such payment platform, the software engineer only need to save the activation key and the order id returned from lemon squeezy. lemon squeezy provide a orders page for the store owners to check order information, as shown below.

Trulli
Fig.1 - The orders page provided by **lemon squeezy**

Cons:

There is no perfect solution in reality. The cons of using lemon squeezy as I observed from my own experiences are:

  • User experience. Rediect to a totally seperate payment page affect user experiences regarding online purchasing in a bad way. In some countries, the page loading speed may need up to 30 seconds.

  • Trust issue. As the domain name for the lemon squeezy store page is different from the source site, it may create trust issue for the potential customers

  • Less control. High managed platform means little spaces for customization. For instance, the layout of the store page UIs are not in the control.

Type 2: Manage everything on your own

For this type of platform, a typical work flow is:

1
select products --> select payment method --> place the order to the platform via RestAPI --> the platform returns a qrcode link for the customer to pay --> nortify the website the payment has completed

in which you have everything in control, i.e. design UIs of products page and payment page, handling all the backend logics.

Pros and Cons

The cons of adopting the type of platform is that it may takes time to build it as everything is under your control. And the pros are clearly the opposite side of the cons of type 1 platform.

Comment and share

Use Async in React Scenarios

in Technology, React Sitevisits: -- Pageviews: --

In a typical software architect that seperates backend (Nodejs) and frontend (React), the frontend is quite ‘light weighted’ in terms of operations regarding data (Also grâce à the tools like react-redux, the frontend development is easier). That means we normally do not need to cope with scenarios that needs to write complex Nodejs code on the frontend side as it is just a viewer. However, due to certain features of React, there are some scenarios that we need to pay attention to. In this article, I summarise two typical scenarios that using ‘async’ within React in a recent project.

Calling Async Method in ‘useEffect’

There are some cases that we need to call async method in useEffct. Can we do things like below:

1
2
3
4
5
useEffect(async () => {
...
await axios.post(...)
...
}, [])

No, we cannot. Why? Because the async axios call returns promise and the useEffect does not expect callback function to return promise. The useEffect waits for the return of nothing or a function.

Then what we should do? There are two methods

  • Define an async function inside useEffect
1
2
3
4
5
6
7
useEffect(() => {
...
async () => {
await axios.post(...)
}()
...
}, [])
  • Define an async function outside useEffect
1
2
3
4
5
6
7
8
9
10
const getData = useCallBack (async () => {
...
await axios.post(...)
...
}, [])

useEffect(() => {
getData()
}, [getData])

Use Async in ‘setInterval’

1
2
3
setInterval(async () => {
await axios(...)
}, 1000)

Can we do things like above in setInterval? It is questionable, as the async call may cost more than the time set in setInterval method. So what is the better option? Use Promise with setTimeout, like below:

1
2
3
4
5
6
7
8
9
10
11
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}

async function loop() {
while (/* condition */) {
/* code to wait on goes here (sync or async) */

await delay(100)
}
}

Reference: https://stackoverflow.com/questions/51830200/how-to-await-inside-setinterval-in-js

The above are the two scenarios that I met in my project, when I look back as a new full-stack engineer (I was heavily involved in backend dev, not frontend):)

Comment and share

Let us start with requirements

Business Requirements

The system needs to accept subcriptions from users with certain period time. After the paid subscription expires, the subscription status in user profile should be downgraded from a subscriped user to a normal user.

Make A Choice among Two Options

MongoDB TTL Index vs Node Schedule

As introduced in one of my previous writing regarding the System Architect , the system uses MongoDB to store user data. To add the function into the system, there were two choices that immediately came to my mind: MongoDB TTL Indexes and Node Schedule

  • Node Schedule
    Node schedule runs on the server side, for example, node-schedule. But since the requirement is to change user’s subscription status after certain amount of time (relatively long, like a week, a month etc.), and it will only run once, putting such burden on the server side for such task is not worth it.

  • MongoDB TTL Indexes
    Compare to Node Schedule, MongoDB TTL Indexes run on the database side, it will not create extra jobs on the server, it is a better choice for such requirement.

MongoDB TTL Indexes

Directly create MongoDB TTL Indexes

Clearly, in my case, I need to create MongoDB TTL Indexes with give parameters after the schema is generated. And after searching for a while, I realised that in mongoose, I can only create TTL indexes when create the schema, it is not possible to create TTL indexes with give parameters when the app is running (please correct me if I am wrong here). Within mongoose, it only can introduce TTL Index within schema definition, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import mongoose from 'mongoose';
const { Schema } = mongoose;

const XXXSchema = new Schema(
{
username: {
type: String,
required: true,
unique: true,
},
},
// timestamp of the creation data of the user
{ timestamps: true }
);

XXXSchema.index(
{ createdAt: 1 },
{
expireAfterSeconds: 30,
partialFilterExpression: { username: { $eq: 'xxx' } },
}
);

Indirectly create MongoDB TTL Indexes

Luckily, there is an alternative method to save my life: using expireAt in the schema. Under the table, the expireAt generates TTL indexes in MongoDB. Below are how I did it:

  1. introduce expireAt in the schema with expires property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import mongoose from 'mongoose'
const { Schema } = mongoose

const XXXSchema = new Schema(
{
username: {
type: String,
required: true,
unique: true,
},
expireAt: {
type: Date,
default: Date.now,
expires: 0,
},
},
// timestamp of the creation data of the user
{ timestamps: true },
)
...
  1. when receiving the parameters, create the data with expireAt is set.
1
2
3
4
5
6
7
8
...
const newXXX = new XXX({
username: user.username,
expireAt: Date.now() + duration * 1000,
});

await newXXX.save();
...
  1. After the code in step 2 is ran, in the MongoDB, a TTL index should be seem. Let us take MongoDB Atlas as an example:
    Image

Job done :)

Comment and share

Recently, I have been busy working on a project start from sctrach – from business idea to a software product running online. I literally complete the design and most of coding job myself (well, I also deeply involded in devops as well). The project is about to go online now. I would like to spend a little bit of time to discuss about the system design of the project.

Business Idea

As it is a chit-chat about system design in real case, I will briefly introduce the business without mentioning too many details.

The system provides a kind of service for registered users that is based on purchased usage quota, i.e., everytime a user consume the service, the number of service usage limit belongs to the user will be reduced by ONE, until it reaches to ZERO. Then the user needs to further purchase the service.

In essence, the system needs:

  • a user management service
  • a mechanism that is able to change the user data in ‘real time’

System Design

Trulli
System Design for the Business

User Management Service

A typical user management module, I chose mongodb for the following 3 reasons:

  • the user schema design was not fixed at the beginning, to avoid data schema changing troubles with relational DB, I went with NoSQL

  • The better sharding and scalability provied by mongodb as it is json document based.

  • To change the user data in ‘real-time’, I consider caching user data in memory for fast I/O, and I only want to cache partial user data, not all of them, therefore, NoSQL is a better option.

Change User Data in ‘Real-time’

As I mentioned, everytime a user consumes the service, the service usage quota will minus ONE. Since the service could be used by multiple users at the same time, the system must not spend too much time on I/O regading changing the user data, hence, I consider use in-memory cache here in stead of updating data directly in mongodb. That is the place where Redis will play.

As shown in the figure, when a user login, the user data will be loaded into Redis, and everytime the user consumes the service, the backend side will update the data in he Redis, and only write back to mongoDB when the user logout.

Sync user data between frontend and backend

At the frontend side, the user may need to see the number of service usage in ‘real-time’. There are two choices:

  • Frontend always keey the data sync with backend, means that everytime the service is used, the frontend will invoke REST API of backend and wait for returned result.
  • Frontend and backend use different data set. To be more specific, the frontend caches the user data in react-redux (only work with data from react-redux), and everytime the user consumes the service, on the frontend side, it change the number in react-redux, at the same time, invoke REST API to update the number in redis on the backend side.

For the 1st choice, the frontend side will always show the correct data but it sacrifices time. For the 2nd one, the frontend may show different data from backend (if something wrong happens on the backend side regarding updating data in redis), but the frontend side does not need to wait for the REST API call result.

I went for the 2nd choice for speed.

Locking the Serivce

The service will be used by multiple users at the same time and it is not sharable. Therefore, I need a distributed lock here.

Since I have introduced redis for caching, I used redis redlock for distributed locking.

These are the design decisions I have made during the project. System design is always about trade-offs: space, time, cost. The most import one: do not over design, the priority is to meet the business requirements not to create a technically perfect product.

Comment and share

Recently, I need to do load tests for a set of REST APIs under Node.js environment. After studying some online articles, I decided to give it a go with AutoCannon. My scenario is that I have already had a sets of requests setup in Postman, and I do not want to rewrite everything for the load test. Luckily, I found a solution that exactly match my requirement (listed below), but I still want to write my own version from a AutoCannon fresher’s perspective and hopefully will be useful for future readers.

Step.1 Export A Collection of Requests from Postman

As shown below, left click the “…” button at the right side of the requests collection. Then choose “Export” in the popup menu.

Trulli
Fig.1 - Export Requests Collection
Afer this step, we should receive a JON file contains all the information of the REST APIs we would like to test.

Step.2 Write Code for Load Testing

We need to create a sepearte xxx.js file that tells AutoCannon what to do.

  • Load request data from exported JSON file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const autocannon = require('autocannon');
const fs = require('fs/promises');

// read array of items from exported .json file from postman
let entries = undefined;

async function getRequests() {
const data = await fs.readFile(
'./youtube_clone_proj.postman_collection.json',
'UTF-8'
);

entries = JSON.parse(data).item;
return true;
}
  • Set up AutoCannon for Each Request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
entries.map(async (entry) => {
// there are multi request in item
entry.item.filter((ele) => {
// filter the empty request
return ele.request.url !== undefined;
}).map(async (ele) => {
console.log(ele.request.method + " " + ele.request.url.raw);
const result = await autocannon({
url: ele.request.url.raw,
method: ele.request.method,
connections: 100,
workers: 50,
duration: 5,
body: ele.request.body === undefined? null : JSON.stringify(ele.request.body),
// read other options here: https://github.com/mcollina/autocannon#autocannonopts-cb
}, finishedBench);

// track process
autocannon.track(result, {renderProgressBar: false});

// this is used to kill the instance on CTRL-C
process.once('SIGINT', () => {
result.stop()
})

function finishedBench (err, res) {
console.log('finished bench', err, res)
}
});

Launch the test

In the terminal window, run the following

1
node xxx.js

Then we should able to see output like this for each invidual request:

Trulli
Fig.2 - API1 result
Trulli
Fig.3 - API2 result
For sure, there are more details left to discover, e.g. settings of _autocannon_, but that is left for reading and searching the official document :)

reference

Benchmark express apis with autocannon from postman collection

Comment and share

Author's picture

Jingjie Jiang


Find a place I love the most