To scale your front-end app, keep your front-end servers stateless.
Huh? I didn't know my front-end servers have states... what do you mean?
A state in this context is any piece of information about users.
Imagine that your app has 2 servers with a load balancer in front of them. User1 visits your app. Load balancer directs it to Server1. User1 logs in.
User1
│
▼
┌─────┐
│ │
│ │
└─────┘
loadbalancer
│
▼
┌─────┐ ┌─────┐
│ │ │ │
│ │ │ │
└─────┘ └─────┘
Server1 Server2
In a stateful application, after user1 logs in, Server1 stores the user information, something like user1: {isLoggedIn: true}
.
User1
│
▼
┌─────┐
│ │
│ │
└─────┘
loadbalancer
│
▼
┌─────┐ ┌─────┐
│ │ │ │
│ │ │ │
└─────┘ └─────┘
Server1 Server2
│
│
user1: {isLoggedIn: true}
With this, each time User1 is connected to Server1, User1 won't have to re-login again. Usually what happens is, after User1 logs in to your site and he needs to visit a login-specific page (like /subscriptions
), the sever would need to validate that it is really User1 (you don't want everyone to be able to change anyone's subscription page).
Without a mechanism to remember that User1 is logged in, he would have to log every single time he goes to a different page. That wouldn't be cool.
What if the next day, when User1 goes to the website again, this time the load balancer directs him to Server2? Well, Server2 doesn't know that User1 logged in yesterday so he would have to login again. Your app tries to look for the isLoggedIn
attribute for User1 inside Server2, but it won't find any. User1 is forced to login again. This is a bad User Experience.
Now let's imagine that you need to scale your application horizontally by adding 5 more servers. At worst case scenario, the load balancer would direct User1 to different servers. User1 would have to login 5 more times.
What if your app is an E-Commmerce app and you store the shopping cart states inside Server1, something like {cart: {red_mug: 1}}
. If User1 doesn't finish his transaction, when he comes back again the next day and the load balancer directs him to Server2, his shopping card is empty! Ok, so he creates a new shopping cart, but this time he has {cart: {purple_mug: 2}}
. He leaves again and the next day he finds out that he has the shopping cart from 2 days ago ({cart: {red_mug: 1}}
) because the load balancer directs him to the Server1.
By storing your states inside your frontend server, the user session is tightly coupled to that particular server. This is not good for scalability. To fix this, you need to make your front-end server as stateless as possible by decoupling the states from the server.
One solution is to have a dedicated server to store the states. Redis is a good option for this.
User1 goes to the website for the first time. The load balancer directs it to the Server2. User1 logs in. The login information is not stored inside Server2, but inside a Redis instance using a token (a token is usually an UUID or anything unique and hard to steal). For the sake of brevity, let's call this token user1_token
(in reality, it would be something like bd1477b6-8655-11eb-8dcd-0242ac130003
).
User1
│
▼
┌─────┐
│ │
│ │
└─────┘
loadbalancer
│
▼ (user1_token good?)
┌─────┐ ┌─────┐ ┌─────┐
│ │ │ ├─────────►│ │
│ │ │ │ │ │
└─────┘ └─────┘◄─────────┴─────┘
Server1 Server2 redis
(token is good!)
- Redis stores that token along with other User1 session information.
- User1 stores that token inside a cookie (or local storage) in the browser (something like
myawesomewebsite_uuid: user1_token
). That's why when you clear your cookie, you often have to re-login to many websites.
Inside User1 browser cookie:
myawesomewebsite_uuid: user1_token
Inside Redis:
user1_token: {isLoggedIn: true, cart: {red_mug: 1}, darkMode: true, lastLoggedIn: '2021-03-01', etc...}
Now when User1 visits the website again, the website will check User1's browser cookie (something like myawesomewebsite_uuid
) and find the unique token user1_token
. Server2 checks with Redis if it has user1_token
or not. If it is found, it fetches User1's session information (cart
, isLoggedIn
, etc) and the server will use that information. If it is not found, User1 will have to login.
With this architecture, it doesn't matter which server the load balancer directs the User1 into. No session state is stored there. Your front-end layer becomes more scalable. If you need to add more servers, just add extra servers into the load balancer pools. People who are already logged in would still be logged in and their cart information would be intact because they are stored in the Redis server.
Have a dedicated location for all your states and separate them from your front-end servers to keep your app scalable.