Mastering System Design for Technical Interviews
Scaling State and Logic
Beyond a Single Server
When an application first launches, it often lives on a single server. This server handles everything: web traffic, business logic, and database queries. It's simple and it works, but as user numbers grow, this lone server will eventually hit its limits. CPU, memory, and network bandwidth all become bottlenecks.
The first instinct is often vertical scaling, or scaling up. This means adding more power to your existing server, like upgrading its CPU, adding more RAM, or installing faster storage. It’s like swapping out a car's engine for a more powerful one. It's straightforward but has two major drawbacks: there’s a physical limit to how powerful a single machine can be, and the cost increases exponentially. Eventually, you can't buy a bigger server.
The alternative is horizontal scaling, or scaling out. Instead of making one server bigger, you add more servers to your network. Traffic is then distributed across this fleet of machines. This approach is far more flexible, cost-effective, and has a virtually unlimited ceiling for growth. It’s the standard for building large-scale systems.
Key takeaway: Design systems to scale horizontally for long-term growth, and understand that vertical scaling has practical limits.
Horizontal scaling introduces a new challenge: how do you manage user-specific data, or state, when a user might interact with a different server for each request? If a user logs in on Server A, Server B has no idea who they are. This leads to a critical design choice between stateful and stateless architectures.
The State of Your Architecture
A stateful architecture is one where the server stores information about the client's past interactions. This session data is kept in the server's memory. If you have a fleet of stateful servers, you must ensure that a user is always sent back to the same server where their session began. This creates dependencies and makes the system brittle.
A stateless architecture, by contrast, treats every request as an independent transaction. The server does not hold any session data between requests. All necessary state is either sent by the client with each request or stored in an external, centralized location that all servers can access. This decouples the servers from the user sessions, making the application tier much easier to scale and manage. If one server goes down, another can seamlessly take over without the user noticing.
The goal for a scalable system is to make the application tier stateless. This allows any server to handle any request at any time.
To achieve this, we need two key components: a load balancer to distribute traffic, and a centralized store to manage sessions.
Directing the Traffic
A load balancer is a server that sits in front of your application servers and routes incoming client requests across the group. Its main job is to ensure no single server becomes overwhelmed. It also plays a vital role in reliability; if a server fails, the load balancer stops sending traffic to it. This prevents a single point of failure in your application tier.
Load balancers use different algorithms to decide where to send each request. The choice of algorithm depends on the specific needs of the system.
| Algorithm | How It Works | Best For | Trade-offs |
|---|---|---|---|
| Round Robin | Distributes requests sequentially to each server in a list. | Simple setups where servers have similar capacity. | Doesn't account for server load or health. A slow request can still hold up one server. |
| Least Connections | Sends the next request to the server with the fewest active connections. | Environments where request processing times vary significantly. | Requires tracking active connections, adding slight overhead. |
| IP Hash | Uses the client's IP address to calculate a hash, which determines which server receives the request. | When you need to ensure a user is consistently routed to the same server (sticky sessions). | Can lead to uneven load distribution if certain IPs send many requests. Doesn't adapt if servers are added or removed. |
Managing User Sessions
Now, let's solve the session state problem. One early approach was sticky sessions. Using a load balancing algorithm like IP Hash, you could "stick" a user to a specific server for their entire session. This keeps the session data on one machine, making it a stateful design.
However, sticky sessions have significant flaws. They create an uneven distribution of load if some users are more active than others. More importantly, if the server a user is stuck to fails, their session is lost. They're logged out and lose any work in progress, like items in a shopping cart.
A much better solution is to externalize the session data. By moving session information out of the application servers and into a centralized data store, the application tier becomes stateless. All servers can read and write session data from this shared location.
A common choice for this is an in-memory cache like Redis or Memcached. These databases are extremely fast because they store data in RAM. When a user logs in, a session ID is created and stored in Redis along with their user data. This session ID is sent to the client as a cookie. For every subsequent request, the client sends the cookie back, the server uses the ID to look up the session in Redis, and the user is authenticated. Any server can now handle the request, because the state lives in Redis, not on the server.
This stateless architecture is the foundation of modern, scalable web services. It allows you to add or remove servers from the pool on the fly without disrupting users, enabling features like auto-scaling to handle traffic spikes automatically.
An e-commerce website is experiencing slowdowns during peak shopping seasons. The engineering team decides to add more servers to distribute the incoming traffic. What is this strategy called?
What is the primary drawback of relying solely on vertical scaling (scaling up)?