09-high-level-design

Scalability and Availability for Backend Systems

A staff-engineer guide to horizontal scaling, stateless services, L4/L7 load balancing, redundancy, and defining availability targets.

August 14, 2026
backend-engineerscalabilityavailabilityload-balancerhorizontal-scalingredundancy

Scalability and Availability

Every high-level design interview and every real production incident eventually comes back to the same two questions: can this system handle more load, and does it keep working when parts of it fail? Scalability and availability are not features you bolt on later — they are architectural decisions baked in from the first component diagram. This guide covers the vocabulary and mechanics a backend engineer with 2-3 years of experience needs to reason about both, and to defend design choices under interview-style questioning.


1. Vertical vs Horizontal Scaling

Vertical scaling (scale up) means adding more resources — CPU, RAM, faster disks — to a single machine. Horizontal scaling (scale out) means adding more machines and distributing load across them. Almost every modern backend system is designed to scale horizontally, because vertical scaling has a hard ceiling and a single point of failure.

Trade-off comparison

DimensionVertical scalingHorizontal scaling
CeilingHard limit (largest instance type available)Effectively unbounded (add more nodes)
Downtime to scaleUsually requires a restart/rebootZero — new nodes join the pool live
Fault toleranceSingle point of failureNode failure is tolerable, traffic reroutes
Cost curveSuperlinear — bigger instances cost disproportionately moreRoughly linear per unit of capacity
ComplexityLow — no distributed state to manageHigher — needs statelessness, load balancing, coordination
Good forDatabases with strong consistency needs, legacy monoliths, quick winsStateless API tiers, web servers, worker pools
💡

These aren't mutually exclusive. Most production systems scale horizontally at the application tier and vertically (plus read replicas or sharding) at the data tier, because databases are far harder to scale out than stateless services. When you're asked "how would you scale this?" in an interview, the strongest answer separates the stateless tier (scale horizontally, trivially) from the stateful tier (scale vertically first, then replicate/shard).

Diminishing returns and Amdahl's Law

Horizontal scaling isn't free linear throughput either. If a request has to touch a shared, non-parallelizable resource — a single-writer database, a global lock, a serial log — that portion caps your speedup no matter how many nodes you add. Amdahl's Law formalizes this: if a fraction p of the work is parallelizable and (1 - p) is serial, the maximum speedup with N nodes is:

text
speedup(N) = 1 / ((1 - p) + p / N)

If 10% of your request path is a serialized write to a single database (p = 0.9), your theoretical maximum speedup caps at 10x, no matter how many app servers you add. This is why "just add more instances" stops working once you hit the database — the real scaling work moves to caching, read replicas, sharding, or async processing.


2. Stateless Service Design

Horizontal scaling only works cleanly if any request can be served by any node. That requires the service tier to be stateless — no request-specific data lives in the memory or local disk of a particular instance between requests.

Where state tries to sneak in

Anti-patternWhy it breaks horizontal scalingFix
In-memory HTTP sessionSession lost if next request lands on a different nodeExternalize to Redis, or use signed stateless JWTs
Local file uploadsFile only exists on the node that received itUpload directly to object storage (S3/GCS), not local disk
In-process caches with write-throughCache diverges between nodes, stale readsShared cache (Redis) or cache invalidation via pub/sub
Sticky sessions at the load balancerDefeats even distribution, hot nodes accumulate long-lived connectionsPrefer stateless auth so any node can serve any request
In-memory rate limiting / countersEach node undercounts, allows N × limit throughCentralized counter (Redis INCR + TTL) or a rate-limiting service
Local cron/scheduled jobsJob runs once per node instead of once totalUse a leader-elected scheduler or an external job runner
⚠️

Sticky sessions are a scaling smell. They're sometimes used as a stopgap when a service has accidental in-memory state, but they concentrate load unevenly, make deploys and autoscaling painful (draining a node with 10,000 pinned sessions is slow), and reintroduce a single point of failure per user. Prefer fixing the statefulness at the source — externalize the state — over papering over it with routing tricks.

Statelessness is also what makes autoscaling safe: a new instance can join the pool and start serving traffic immediately with zero warm-up beyond process startup, and an instance can be terminated at any time without losing in-flight user context (as long as in-flight requests are drained, which is a separate concern — see failover below).


3. Load Balancers: L4 vs L7

A load balancer distributes incoming traffic across a pool of backend instances. The critical distinction for interviews is which OSI layer it operates at, because that determines what it can see and what decisions it can make.

L4 vs L7 comparison

AspectL4 (Transport)L7 (Application)
Operates onIP address + TCP/UDP portFull HTTP request (path, headers, cookies, method)
Routing decisionsWhich server gets this connectionPath-based, header-based, host-based routing
PerformanceVery fast — no payload inspectionSlower — must terminate/parse the request
TLS terminationUsually passes through (or terminates at TCP level)Commonly terminates TLS to inspect the request
ExamplesAWS NLB, LVS, HAProxy (TCP mode), IPVSAWS ALB, NGINX, Envoy, HAProxy (HTTP mode)
Use whenRaw throughput matters, non-HTTP protocols (gRPC over raw TCP, databases)Content-based routing, A/B testing, canary releases, WAF rules
Sticky sessionsBased on IP/connectionBased on cookies (application-aware)

Interview shorthand: L4 load balances connections, L7 load balances requests. A single long-lived HTTP/2 or gRPC connection can carry many logical requests — an L4 balancer sends them all to the same backend, while an L7 balancer can distribute individual requests across backends even within the same connection. This matters a lot for gRPC services behind a plain TCP load balancer, which is a classic "why is my traffic unbalanced" production bug.

A minimal L7 config, for concreteness

nginx
upstream orders_service {
    least_conn;                          # route to the backend with fewest active connections
    server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
    keepalive 64;                        # reuse connections to upstreams
}
 
server {
    listen 443 ssl;
    server_name api.example.com;
 
    location /orders/ {
        proxy_pass http://orders_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        proxy_next_upstream error timeout http_502 http_503;  # retry on next node
    }
}

Load balancing algorithms

AlgorithmBehaviorBest for
Round robinCycles through servers in orderUniform request cost, homogeneous servers
Least connectionsSends to the server with fewest active connectionsVariable request duration (e.g., some requests slow, some fast)
Weighted round robinRound robin with per-server weightsHeterogeneous instance sizes
IP hash / consistent hashSame client (or key) always routes to the same backendSession affinity, cache locality
Least response timeCombines connection count with observed latencyLatency-sensitive services with variable backend health

4. Reverse Proxies vs Load Balancers vs API Gateways

These three terms get used interchangeably in casual conversation but have distinct primary responsibilities, even though a single piece of software (NGINX, Envoy) often plays more than one role at once.

ComponentPrimary jobSits whereTypical extras
Reverse proxySits in front of one or more origin servers, forwards client requests to themEdge of a service or a whole siteTLS termination, static asset caching, compression, request buffering
Load balancerDistributes traffic across a pool of identical backend instancesBetween a proxy/gateway and a service's instancesHealth checks, connection draining, weighted routing
API gatewaySingle entry point for many different backend services, applies cross-cutting policyEdge of the whole system, in front of many servicesAuthN/AuthZ, rate limiting, request/response transformation, aggregation, API versioning
💡

In a microservices system these three responsibilities are usually layered: an API gateway (Kong, AWS API Gateway, Envoy-based) handles auth and routing to the right service, and each service sits behind its own load balancer distributing across its replica pool. In a smaller monolithic deployment, a single NGINX instance often plays all three roles at once — that's fine, but be able to name which responsibility a config block is serving when asked.


5. Redundancy and Failover

Redundancy means running more capacity than the minimum needed, specifically so that the failure of any single component doesn't take down the system. Failover is the mechanism that detects a failure and redirects traffic away from it.

Levels of redundancy

LevelWhat's duplicatedProtects against
Instance-levelMultiple app server replicas behind a load balancerSingle process crash, OOM, deploy issue
Zone-level (AZ)Instances spread across availability zonesA single data center losing power/network
Region-levelFull stack deployed in multiple geographic regionsRegional outage, natural disaster
Data-levelDatabase replicas (sync or async)Primary DB node failure, disk corruption
Dependency-levelFallback for a third-party API (cache, degraded mode)Upstream vendor outage
⚠️

Redundancy without health checks is theater. A load balancer with three backends is not actually redundant if it keeps sending traffic to a dead instance for 60 seconds before noticing. The health check interval and failure threshold directly determine your effective MTTR (mean time to recovery) for a single-node failure — tune them deliberately (e.g., 3 failures at 5s intervals = ~15s detection), and make sure the check hits a real dependency chain (/health should verify DB connectivity, not just "process is up").

Active-passive vs active-active

ModelDescriptionFailover timeCostComplexity
Active-passiveStandby replica is idle until primary failsSeconds to minutes (needs promotion)Lower — standby is underutilizedSimpler — no split-brain risk if done right
Active-activeAll replicas serve live traffic simultaneouslyNear-zero — traffic already flowing elsewhereHigher — all nodes fully provisionedHarder — needs conflict resolution / partitioning for writes

Most real systems mix both: active-active for reads (any replica can serve a read), active-passive for writes (single writer, promoted on failure). This is exactly the shape of a typical primary/replica relational database setup, and it's a strong default answer when an interviewer asks "how do you make the database highly available?"

Graceful degradation over hard failure

Redundancy handles component loss; graceful degradation handles overload or partial dependency failure without a full outage:

  • Circuit breakers — stop calling a failing downstream dependency after an error threshold, fail fast instead of piling up threads waiting on timeouts.
  • Bulkheads — isolate resource pools (thread pools, connection pools) per dependency so one slow dependency can't starve the whole service.
  • Load shedding — reject low-priority requests first when the system is over capacity, to protect the requests that matter.
  • Fallback responses — serve stale cached data or a reduced feature set rather than an error page (e.g., show cached recommendations if the personalization service is down).

6. Defining and Measuring Availability

"Highly available" is meaningless without a number attached. Availability is conventionally expressed as a percentage of uptime over a period, and colloquially referred to by its "nines."

The nines table

AvailabilityDowntime / yearDowntime / monthDowntime / week
99% ("two nines")3.65 days7.2 hours1.68 hours
99.9% ("three nines")8.76 hours43.2 minutes10.1 minutes
99.95%4.38 hours21.6 minutes5 minutes
99.99% ("four nines")52.6 minutes4.32 minutes1.01 minutes
99.999% ("five nines")5.26 minutes25.9 seconds6.05 seconds
🚨

Five nines is extraordinarily expensive and rarely the right target. It implies your entire stack — including every third-party dependency, DNS, your cloud provider's control plane, and your deploy process — never causes more than ~5 minutes of cumulative downtime in a year. Most consumer-facing products target 99.9%–99.95%; core payment or infrastructure paths might justify 99.99%. Always ask "what does the business actually lose per minute of downtime?" before committing to a nines target — it should come from a cost/risk conversation, not be picked because it sounds impressive.

SLI, SLO, SLA, and error budgets

TermDefinitionExample
SLI (Indicator)A measured metric of service behavior"% of requests completed in < 300ms", "% of requests returning non-5xx"
SLO (Objective)An internal target for an SLI"99.9% of requests succeed over a rolling 30 days"
SLA (Agreement)An external, often contractual, commitment — usually looser than the internal SLO"99.5% uptime or customer receives service credits"
Error budget1 - SLO, the amount of unreliability you're allowed to spend99.9% SLO → 0.1% error budget → ~43 min/month you can "spend" on deploys, experiments, risk
💡

The error budget is a planning tool, not just a report card. If you've burned your error budget for the month, the standard practice is to freeze risky changes (new feature launches, risky migrations) and prioritize reliability work until the budget resets. If you're well under budget, that's a signal you can afford to ship faster and take more risk. This reframes "reliability vs velocity" from a philosophical debate into a number both engineering and product can agree on.

Availability of a composed system

When a request depends on multiple components in series, availabilities multiply — this is a common interview gotcha.

text
Overall availability = A1 × A2 × A3 × ... × An   (for components in series)

For example, a request that must succeed through an API gateway (99.99%), an app service (99.95%), and a database (99.95%):

text
0.9999 × 0.9995 × 0.9995 ≈ 0.9989  →  99.89% overall, NOT 99.95%

Chaining "highly available" components in series reduces overall availability. This is exactly why redundancy at each hop (so each hop's effective availability approaches 100% via failover) matters more as the request path gets longer — and why deeply nested microservice call chains are an availability risk in their own right, independent of any single service's reliability.

For components that are genuinely redundant in parallel (e.g., either replica can serve the request), availability improves:

text
Availability(parallel) = 1 - [(1 - A1) × (1 - A2)]

Two independent nodes each at 99% availability, deployed in parallel with automatic failover, together achieve 1 - (0.01 × 0.01) = 99.99% — this is the mathematical justification for "just add a standby."


7. Back-of-Envelope Capacity Planning

Scalability decisions should be grounded in numbers, not vibes. A standard capacity estimate for an interview or a real capacity-planning doc:

text
Given:
  - 10 million daily active users (DAU)
  - Each user makes ~20 requests/day on average
  - Traffic is not uniform: peak = 3x average

Average requests/day = 10,000,000 × 20 = 200,000,000
Average requests/sec = 200,000,000 / 86,400 ≈ 2,315 req/s
Peak requests/sec    = 2,315 × 3 ≈ 6,945 req/s

If each app server instance safely handles 500 req/s at target latency:
  Instances needed at peak = 6,945 / 500 ≈ 14 instances

Add N+2 redundancy for zone loss / rolling deploys → provision ~16-18 instances

Interviewers care less about the exact numbers and more about whether you (1) state your assumptions explicitly, (2) account for peak-vs-average skew, and (3) connect the estimate back to a concrete decision — like instance count, cache size, or database read-replica count. A capacity estimate that doesn't change any design decision wasn't worth doing.


8. Production Observations

  • Autoscaling reacts to metrics, it doesn't predict them. Scale-out based on CPU or request-rate has a lag (metric collection interval + new instance boot time + warm-up). For predictable traffic spikes (e.g., a sale event at a known time), schedule pre-scaling ahead of the event rather than relying purely on reactive autoscaling.
  • Connection pool exhaustion, not CPU, is the most common real-world scaling bottleneck. Doubling app server instances without raising the database's max connection limit (or adding a connection pooler like PgBouncer) just moves the bottleneck to too many connections errors at the database.
  • Health checks need to check the right thing. A /health endpoint that returns 200 OK unconditionally provides zero signal. It should verify the dependencies the instance actually needs (DB reachable, cache reachable) without becoming so expensive it causes false failures under load.
  • Load balancer timeouts should be shorter than client timeouts, and backend timeouts shorter than load balancer timeouts. Otherwise the client gives up and retries while the backend is still working, doubling load during exactly the period the system is struggling.
  • Zone-level redundancy is table stakes; region-level is a deliberate, costly decision. Multi-region active-active systems introduce data replication lag and conflict resolution complexity — don't reach for it unless the business case (regulatory residency, latency to a global user base, disaster recovery RTO) justifies it.
  • Every "nines" target implies an operational cost — on-call rigor, deployment safety (canaries, automated rollback), and testing (chaos engineering, game days). Don't commit to 99.99% in a design doc without the operational muscle to back it.

Key takeaways

  • Horizontal scaling is the default for stateless service tiers; vertical scaling and replication/sharding carry the data tier.
  • Statelessness (externalized sessions, no local-disk dependency, centralized rate limits) is the prerequisite that makes horizontal scaling and autoscaling safe.
  • L4 balances connections, L7 balances requests — know which one you're being asked about, since it changes what routing decisions are even possible.
  • Redundancy only pays off if health checks and failover actually detect and route around failures fast enough to matter.
  • Availability of components chained in series multiplies down; parallel redundancy multiplies failure probabilities down — always distinguish which topology you're computing for.
  • SLOs and error budgets turn "how reliable should this be" into a number that governs both incident response and release velocity.
  • Capacity numbers should always tie back to a decision (instance count, cache size, shard count) — an estimate that changes nothing wasn't worth doing.

Interview Questions

  • What is the difference between vertical and horizontal scaling, and when would you choose one over the other?
  • Why does horizontal scaling require the service layer to be stateless? Give three examples of hidden state that breaks it.
  • Explain the difference between an L4 and an L7 load balancer. Which would you use for a gRPC service, and why does the connection-vs-request distinction matter there?
  • What's the difference between a reverse proxy, a load balancer, and an API gateway?
  • How would you design failover for a service with three replicas across two availability zones?
  • What's the difference between active-active and active-passive redundancy? What are the trade-offs?
  • Define SLI, SLO, SLA, and error budget. How does an error budget change how a team ships features?
  • If Service A depends on Service B and Service C in series, and each is individually 99.9% available, what is the overall availability of a request through all three? What does that imply about deep microservice call chains?
  • How would you estimate the number of application server instances needed to serve a given peak request rate?
  • What is Amdahl's Law, and how does it explain why "just add more servers" eventually stops helping?
  • Why is a naive /health endpoint that always returns 200 dangerous in production?
  • Describe the difference between graceful degradation and a circuit breaker. How do they work together under partial outage?
  • What operational practices are required to responsibly claim a 99.99% availability target?
  • How would you detect and mitigate connection pool exhaustion when scaling out an application tier?