Phase 1: Foundations

Horizontal vs vertical scaling & stateless design

Intermediate ~4 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you’ve opened the most amazing cookie shop ever, and everyone wants your delicious cookies! At first, you have one oven and one baker, and things are great. But soon, so many customers are lining up that your single oven just can’t bake enough cookies fast enough. How do you keep everyone happy and get more cookies out the door?

One way is to buy a super giant, super powerful oven. It can bake a huge batch of cookies all at once! This is like making one thing bigger and stronger to handle more work, which in computer talk we call "vertical scaling." It might seem like a good idea, but even the biggest ovens have a limit to how many cookies they can bake, and if that one giant oven breaks down, suddenly no one gets cookies until it’s fixed! And usually, upgrading to a bigger oven means you have to close the shop for a while.

Another way is to buy lots of smaller, identical ovens. When more customers show up, you just add another oven! This is like adding more separate machines to share the work, which we call "horizontal scaling." Now, if one oven breaks, the others can keep baking. And you can add new ovens without ever closing your shop. The secret to making this work smoothly is how you handle your cookie orders.

Think about those order slips. Each slip must have all the details needed for the cookies: "Customer A wants one dozen chocolate chip cookies." When a baker (who is like a computer server) picks up an order slip, they don't need to remember what Customer A ordered last week or what they said on the phone. All the information is right there on the slip. We call this "stateless design" – the baker doesn't keep a special memory for each customer in their own head. This means any available baker can grab any order slip and start baking immediately. So, when you design computer systems this way, you can easily add more "bakers" (servers) whenever demand grows, making sure your "cookie shop" stays fast and reliable no matter how popular it gets!

When designing systems to handle varying loads, two primary scaling strategies emerge: vertical and horizontal. Vertical scaling, often called "scaling up," involves increasing the resources (CPU, RAM, disk I/O) of a single server. While straightforward to implement initially, it faces inherent limitations: there's a finite maximum capacity for any single machine, upgrading typically requires downtime, and a single server remains a single point of failure. In contrast, horizontal scaling, or "scaling out," means adding more individual servers or instances to a cluster, distributing the load across them. This approach offers superior fault tolerance and near-limitless scalability, as you can continuously add more nodes as demand grows.

For horizontal scaling to be effective, your application services must be stateless. A stateless service means that each request from a client contains all the necessary information for the server to fulfill it, and the server itself does not store any client-specific session data in its local memory between requests. If a service stored session information locally, adding more servers would be problematic: a user's subsequent request might hit a different server that doesn't have their stored state, leading to a broken user experience. By designing services to be stateless, any available server can process any request at any time, making it easy to add or remove servers without affecting ongoing user interactions.

SREs frequently manage horizontally scaled, stateless systems because they form the foundation of resilient and highly available infrastructure. This architectural pattern allows for robust deployments (e.g., blue/green, canary releases), quick recovery from node failures (simply replace a faulty instance, as no critical state is lost), and efficient auto-scaling in cloud environments. While stateless applications simplify scaling, SREs must also ensure the high availability and consistency of the external data stores (like databases, message queues, or distributed caches such as Redis) where the actual application state is persisted. Understanding and implementing these concepts is crucial for building and maintaining robust distributed systems.

Key Takeaways

  • Vertical scaling enhances single server power; horizontal scaling adds more servers for distributed load.
  • Horizontal scaling provides better resilience, fault tolerance, and greater scalability limits.
  • Stateless design is fundamental for effective horizontal scaling, ensuring any server can handle any request.
  • Application state should be externalized to shared, highly available data stores (e.g., databases, Redis).
  • SREs manage the infrastructure that enables these scalable, resilient, and distributed systems.

Code Example

python
# Conceptual comparison: how state is handled for scaling

# --- Stateful Service (Problematic for horizontal scaling) ---
# Each server instance has its own, independent 'in_memory_cache'.
# User A hits Server 1, then Server 2 -> state lost on Server 2.
in_memory_cache = {}
def handle_stateful(user_id):
    if user_id not in in_memory_cache:
        in_memory_cache[user_id] = {"count": 0}
    in_memory_cache[user_id]["count"] += 1

# --- Stateless Service (Ideal for horizontal scaling) ---
# All server instances connect to a single, shared, external data store.
# (e.g., Redis, a database). Any server can access User A's state consistently.
class MockRedis: # Simulates an external store
    _data = {}
    def get(self, key): return self._data.get(key, {"count": 0})
    def set(self, key, value): self._data[key] = value
shared_redis_client = MockRedis()

def handle_stateless(user_id):
    # Retrieve state from the shared external store
    user_state = shared_redis_client.get(f"user:{user_id}:session")
    user_state["count"] += 1
    # Store updated state back to the shared external store
    shared_redis_client.set(f"user:{user_id}:session", user_state)

How this code works

The provided code illustrates the fundamental difference between stateful and stateless service designs, crucial for understanding horizontal scaling. The stateful example, using in_memory_cache within the handle_stateful function, shows a common pitfall: each server instance would manage its own isolated cache. If a user interacts with Server 1, their count increases there. If their next request goes to Server 2, Server 2 has no knowledge of Server 1's in_memory_cache, effectively losing the user's progress. This design inherently limits how many servers can process requests for the same user without data inconsistencies.

In contrast, the stateless design, exemplified by handle_stateless, overcomes this by relying on an external, shared data store. The MockRedis class simulates such a store, representing a centralized database or caching service accessible by all servers. When handle_stateless processes a request, it uses shared_redis_client.get to retrieve the user's current state from this central location, updates the count, and then uses shared_redis_client.set to write the updated state back. A subtle but important detail is that MockRedis.get provides a default {"count": 0} if a user's data isn't found, ensuring consistent initialization of user state regardless of which server handles the request. This allows any server to serve any user request correctly and consistently, making horizontal scaling straightforward.