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

  • page 1 of 1
Author's picture

Jingjie Jiang


Find a place I love the most