CodeOath
← All posts
System Design52 min total · 14 parts

System Design Fundamentals for Interviews: Scalability, Trade-offs, and the Framework Interviewers Actually Grade

Part 3 of 14 · ~4 min

Scalability Fundamentals

"Scalability" gets used loosely, but it has a precise meaning: a system's ability to handle growth — in users, in data, in traffic — by adding resources, ideally without a fundamental redesign. There are exactly two axes you can scale along, and the choice between them shapes almost everything else in this reference.

Vertical Scaling

Vertical scaling (scaling up) means making a single machine more powerful — more CPU, more RAM, faster disks. It's the simplest possible scaling strategy because it changes nothing about your application's architecture: the code that ran on the smaller machine runs unmodified on the bigger one.

It also runs into a hard ceiling fast. There's a largest machine you can rent or buy at any given time, and even before you hit it, cost stops scaling linearly — doubling a machine's capacity often costs well more than double, because top-of-line hardware carries a premium the mid-range tier doesn't. Worse, a single machine is a single point of failure no matter how powerful it is: it goes down, the whole system goes down, full stop.

Horizontal Scaling

Horizontal scaling (scaling out) means adding more machines and spreading the load across them, rather than making any one machine bigger. This is the approach that has no real ceiling — need more capacity, add another server — and it comes with a redundancy bonus vertical scaling can never offer: lose one of ten servers and the other nine keep the system running, degraded but alive, instead of taking the whole thing down.

The catch is that horizontal scaling isn't free just because you decided to want it. It requires a way to distribute requests across the fleet (load balancing, next chapter) and, much more fundamentally, it requires the application servers receiving those requests to actually be interchangeable.

Statelessness Is What Makes Horizontal Scaling Actually Work

This is the idea that ties the whole chapter together, and it's worth being explicit about because it's easy to nod past: horizontal scaling is only possible at all because a stateless service can have any request routed to any instance and get an identical result, since no instance is holding onto information the others lack.

A stateful service breaks that guarantee. Imagine an application server that keeps a logged-in user's session data — their cart contents, their auth state — in that server's own local memory. The very next request from that same user has to land on that same server, or the session data simply isn't there. Now your load balancer isn't free to route wherever's least busy; it has to track which user belongs to which server (session affinity / "sticky sessions"), and the moment that server goes down, every session pinned to it is gone with it. You've reintroduced a version of the single-point-of-failure problem vertical scaling had, just distributed across more machines.

The fix, and the standard pattern in real systems, is to push that state out of the application server entirely — into a shared store (a database, a distributed cache like Redis) that every instance can reach identically:

Stateful (fragile):
  Client -> Load Balancer -> Server A (holds session in local memory)
  Next request MUST return to Server A, or the session is gone.

Stateless (scalable):
  Client -> Load Balancer -> any Server (A, B, or C — doesn't matter)
                                  |
                                  v
                          Shared session store (Redis / DB)
  Any server can serve any request, because none of them are
  the only copy of anything.

Once every application server is interchangeable, the load balancer is free to send a request to whichever instance is actually available and least loaded, new instances can be added or removed on the fly without any coordination about "who's holding what," and a crashed instance costs you nothing but its own in-flight requests — nothing it uniquely knew is lost, because nothing about it was unique.

Common mistake: Designing a system that "just adds more servers" for scale while quietly keeping meaningful state — an in-memory cache of computed results, a queue of pending work — local to each instance. It looks stateless on the diagram and isn't, and the failure shows up as an intermittent, hard-to-reproduce bug the first time two requests from the same user land on two different servers.