ArchitectMind
  1. Home
  2. Tutorials
  3. Book 8 · Architecture Quality Attributes
  4. Volume III
  5. Horizontal Scaling
Book 8 · Volume III — Scalability & Elasticity · Topic 1.3

Horizontal Scaling

More instances · load balancing

10 min readStage 3 · Systems10 interview questions
Chapter
Chapter 1 — Scalability Fundamentals
Prerequisites
Topics 1.1, 1.2
You will be able to
Scale services by adding identical instances, satisfy the prerequisites (statelessness, load balancing, scalable shared state, automation, idempotency), distribute instances across AZs, predict where horizontal gains stop, and plan horizontal scaling for every tier
Covers
More instances · Load balancing
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — Where Gains Stop
  5. 4A. Deep Dive — Scale-Out Mechanics in Kubernetes
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

Horizontal scaling (scaling out) adds more instances of a component and spreads work across them, usually behind a load balancer. Capacity grows with instance count rather than instance size.

Horizontal scaling
Figure 1 — Horizontal scaling
ElementRole
More instancesIdentical, interchangeable copies (pods, tasks, VMs)
Load balancingDistributes requests across instances (Chapter 3)
Shared stateDatabases, caches, queues accessed by all instances
AutomationAdds/removes instances (Chapter 4)
Table 1 — Parts of horizontal scaling

2Why It Exists

Every instance has a ceiling; fleets do not. Horizontal scaling also gives redundancy (losing one of 60 instances costs 1.7% of capacity), allows scaling in with demand, and uses cheaper commodity or spot capacity.

Horizontal scaling — pros and cons
Figure 2 — Horizontal scaling — pros and cons
BenefitExample
Capacity beyond one machineSearch at 22k req/s on 40–110 pods
RedundancyPod or node loss invisible
Elasticity24 pods at night, 66 at sale
CostSpot/Graviton nodes, scale-in off-peak
Deployment safetyRolling updates and canaries across instances
Table 2 — Why scale out

3Core Concepts

3.1 More Instances

Prerequisites for horizontal scaling
Figure 3 — Prerequisites for horizontal scaling
Horizontal capacity — pods vs throughput
Figure 4 — Horizontal capacity — pods vs throughput
Instance placement ruleReason
Spread across 3 AZs (topology spread)AZ failure tolerance
Anti-affinity across nodesNode failure tolerance
PodDisruptionBudgetsMaintenance can't remove too many at once
Identical config/imagesInterchangeability
Graceful shutdownNo dropped in-flight requests on scale-in
Table 3 — Placing instances

3.2 Load Balancing

Role in horizontal scalingDetail
Entry pointOne address for N instances
DistributionSpread load evenly (algorithms, Chapter 3.2)
HealthOnly route to healthy instances (3.3)
Elastic membershipRegister/deregister as instances come and go
Connection drainingFinish in-flight requests on removal
Table 4 — Load balancing makes instances invisible to clients
Horizontal scaling at every tier
Figure 5 — Horizontal scaling at every tier

4How It Works Internally — Where Gains Stop

Adding instances — where gains stop
Figure 6 — Adding instances — where gains stop
Shared bottleneckSymptom when scaling outFix
DB writerThroughput flattens, DB CPU/locks riseBatch writes, partition (Z)
DB connectionsPool × pods > max_connectionsProxy, budgets
Cache hot keysOne shard saturatesL1 cache, key splitting
Downstream quotaPartner TPS limitSecond provider, queueing
Kafka partitionsConsumers idle beyond partition countMore partitions
Coordination (locks, leader)Coherence overheadPartitioned ownership
Table 5 — Shared bottlenecks
Signal that horizontal scaling is workingSignal that it is not
Throughput per pod roughly constant as pods increaseThroughput per pod falls steadily
p95 flat after scale-outp95 unchanged or worse after adding pods
Shared-tier utilisation grows proportionallyOne shared resource saturates (writer, shard, partner)
Scale-in without errors5xx spikes on scale-in
Table 6 — Reading scale-out results

4ADeep Dive — Scale-Out Mechanics in Kubernetes

Scaling out and in over a day
Figure 7 — Scaling out and in over a day
StepTime
Metric crosses target (HPA)15–30 s evaluation
Pods createdseconds (if nodes available)
Nodes provisioned (Karpenter)60–120 s
Image pull, JVM start, readiness20–60 s (+ warm-up)
Registered in LB target group≈ 10–20 s after readiness
Scale-in: deregistration + drain30 s deregistration delay, graceful shutdown
Table 7 — Where the minutes go (details in Chapter 4)

5APIs / Features / Syntax

YAML
# Deployment prepared for horizontal scaling
apiVersion: apps/v1
kind: Deployment
metadata: { name: checkout }
spec:
  replicas: 24
  strategy: { rollingUpdate: { maxSurge: 25%, maxUnavailable: 0 } }
  template:
    spec:
      terminationGracePeriodSeconds: 45
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector: { matchLabels: { app: checkout } }
      containers:
        - name: checkout
          readinessProbe: { httpGet: { path: /actuator/health/readiness, port: 8080 } }
          # let LB deregister
          lifecycle: { preStop: { exec: { command: ["sleep", "10"] } } }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: checkout }
spec: { maxUnavailable: 10%, selector: { matchLabels: { app: checkout } } }
YAML
# Spring Boot 4: graceful shutdown so scale-in never drops requests
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
JAVA
// Idempotent handler: retries may land on a different instance
@PostMapping("/orders")
ResponseEntity<OrderRef> place(@RequestHeader("Idempotency-Key") String key,
                               @RequestBody PlaceOrder cmd) {
    // stored result, any instance
    return idempotency.execute(key, () -> orders.place(cmd))
                      .map(ResponseEntity::ok).orElseThrow();
}

6Practical Examples

6.1 ShopNow checkout scale-out

PodsThroughput (p95 ≤ 800 ms)Efficiency
8470 req/s98%
1688094%
321,52081%
572,65079%
663,06079% (AZ reserve)
Table 8 — Near-linear until the writer contention (fixed by batching)

6.2 Hidden state blocked scaling

A rate limiter kept counters in pod memory; scaling from 20 to 60 pods tripled the effective limit and partners exceeded their quotas. Moving counters to Valkey restored correct limits regardless of pod count.

6.3 Consumers limited by partitions

The inventory consumer group was scaled from 12 to 24 pods, but orders.events had 12 partitions — 12 pods idled. Partitions were increased to 48 before the next sale.

6.4 Worked example — prerequisites audit

PrerequisiteCheckoutSeller portal (legacy)
Stateless✔✘ in-memory sessions → Spring Session + Valkey
Behind LB✔ ALB✔ ALB with stickiness (to remove)
Shared state scales✔ proxy, replicas✘ single DB pool of 10
Autoscaling✔ HPA✘ fixed 4 VMs
Idempotent writes✔Partly
Table 9 — Audit before scaling out

6.5 Worked example — capacity loss per instance

Fleet sizeCapacity lost per instance failure
333%
1010%
601.7%
Table 10 — More, smaller instances make failures cheaper

6.6 Worked example — horizontal plan per tier

TierScale-out unitCurrentNext step
Checkoutpod (8 vCPU)24–89max 110
Searchpod (4 vCPU)40–110OpenSearch shards 18
Valkeyshard (xlarge)612 shards
Orders DB readsAurora reader2auto-scaling 2–5
Kafkapartition + consumer48 / 24—
Table 11 — Every tier has its own unit

6.7 Worked example — interview whiteboard

PromptAnswer outline
"How would you scale this API horizontally?""Make it stateless (tokens, external session/cart), put it behind an L7 load balancer, spread replicas across AZs with PDBs, add HPA/Karpenter, graceful shutdown and idempotency keys. Then check shared tiers: DB connections via a proxy, read replicas, cache sharding, partitions — and measure scaling efficiency to find the next bottleneck."
Table 12 — Structured answer

6.8 Worked example — spot capacity for stateless tiers

TierOn-demand shareSpot shareSafeguards
Search pods40%60%PDB, diversified instance types, 2-min interruption drain
Checkout pods100%0% (Tier 0)—
Batch workers0%100%Checkpointing, retries
Saving—≈ 38% on search compute—
Table 13 — Horizontal scaling makes cheap interruptible capacity usable

6.9 Worked example — scale-in safety test

TestResult before fixesAfter
Scale 60 → 24 pods under load0.7% 502/504 errors0.01%
CauseNo preStop; ALB still routed to terminating podspreStop 10 s + graceful shutdown 30 s
Table 14 — Scale-in must be as safe as scale-out

6.10 Worked example — horizontal scaling in ECS (alternative platform)

SettingValue
Service desired count12 (min) – 60 (max)
Target trackingALBRequestCountPerTarget = 45 req/s
Capacity providerFargate + Fargate Spot (base 12 on Fargate)
DeploymentRolling, minimumHealthyPercent 100, maximumPercent 150
Deregistration delay30 s
Table 15 — Same principles, different platform

6.11 Worked example — memory and connections scale with instances

PodsHikari connections (20/pod)Valkey connections (8/pod)Via proxy to DB
24480192~120
661,320528~220
1102,200880~260 (proxy limit 30% of max_connections)
Table 16 — Every scale-out multiplies connections to shared tiers — budget them

6.12 Worked example — scheduled jobs in a scaled-out service

ProblemFix
@Scheduled cleanup ran on all 66 pods (66× DB load)ShedLock (DB/Valkey lock) or a dedicated CronJob
Report generation per podSingle job queue consumed by workers
Table 17 — Horizontal scaling multiplies anything each instance does on its own

7Common Problems & Mistakes

MistakeFix
Scaling stateful instancesExternalise state first
Forgetting shared tiersScale DB, cache, partitions too
All replicas in one AZTopology spread
Dropped requests on scale-inGraceful shutdown, preStop, draining
Non-idempotent retries across instancesIdempotency keys
Local counters/limitsShared stores
Table 18 — Mistakes and fixes

8Performance & Production Considerations

ConsiderationPractice
Efficiency dashboardThroughput per pod vs count
QuotasEC2 vCPU, EKS IPs, ALB targets
Spot capacityFor stateless tiers with PDBs and diversification
CostScale-in off-peak; right-size pods
Table 19 — Operating scale-out

Scaling out in production is limited as often by platform quotas as by application design, so check them before every peak. EC2 vCPU limits per instance family, IP addresses available to the VPC CNI in each subnet, ALB targets per target group and Aurora max_connections all cap how far a fleet can grow, and raising a service quota can take days. Track scaling efficiency on a dashboard, plotting throughput per pod against pod count: a falling curve means a shared tier is binding long before latency alarms fire. Use spot capacity only for stateless, interruption-tolerant tiers, with several instance types and a PodDisruptionBudget so a reclaim wave cannot remove too many replicas at once. Finally, scale in deliberately off-peak and right-size pod requests, because an over-provisioned fleet of 110 pods costs the same whether or not traffic arrives.

9Architecture & Design Considerations

GuidelineDetail
Design stateless by defaultChapter 2
Partition shared state earlyKeys, tenants
Plan per-tier scaling unitsPods, shards, partitions, readers
Test with instance lossChaos: kill pods under load
Table 20 — Design guidance

10Interview Questions & Answers

Q1BasicConcept

What is horizontal scaling?

Horizontal scaling, or scaling out, adds more identical instances of a component and spreads work across them, usually behind a load balancer, so capacity grows with the number of instances rather than the size of each one. In Kubernetes that means more pods of a Deployment, managed by the HPA and with nodes added by Karpenter; in ECS, a higher desired task count. ShopNow's search runs 40 to 110 pods and serves 22,000 requests per second. Beyond capacity, it gives redundancy, because losing one of 60 pods costs only 1.7% of capacity, elasticity to scale in at night, and the ability to use cheaper spot or Graviton capacity.

Q2BasicConcept

What is its main prerequisite?

The main prerequisite is stateless, interchangeable instances: any instance must be able to serve any request, so nothing a client needs may live only in one instance's memory or disk. Sessions, carts, rate-limit counters, uploaded files and scheduled-job ownership must move to shared stores such as Valkey, a database or S3, and identity should travel in tokens. If that is not done, the load balancer needs sticky sessions, scale-in loses user state and pod failures log people out. ShopNow's legacy seller portal kept sessions in memory and moved them to Spring Session with Valkey before it could scale. The other prerequisites are load balancing, scalable shared state, automation and idempotency.

Q3IntermediateConcept

Why is a load balancer required?

A load balancer gives clients one stable address while the set of instances behind it changes constantly. It spreads requests across instances with an algorithm such as round robin or least outstanding requests, routes only to instances that pass health checks, and registers and deregisters instances as the autoscaler adds or removes them. During scale-in or deployments it drains connections so in-flight requests complete. Without it, clients would need to know every instance and react to failures themselves. At ShopNow an ALB fronts the services, with target groups fed by the AWS Load Balancer Controller, and inside the cluster Kubernetes Services do the same job for internal calls.

Q4IntermediateInternals

Where does horizontal scaling stop helping?

Horizontal scaling stops helping when the bottleneck moves to something all instances share. The usual culprits are the database writer, whose CPU and locks rise while throughput flattens; database connections, where pool size multiplied by pod count exceeds max_connections; hot cache keys that saturate one Valkey shard; partner API quotas; Kafka partition counts that leave extra consumers idle; and coordination such as distributed locks or leader election. The Universal Scalability Law describes this: contention and coherence costs make throughput per instance fall as you add instances. The signal is falling throughput per pod or flat p95 after scale-out, and the fix is to scale or partition the shared tier, not to add pods.

Q5Senior EngineerScenario

How do you avoid dropping requests when scaling in?

Make removal a coordinated sequence so the load balancer stops sending traffic before the application stops accepting it. When Kubernetes terminates a pod, the endpoint removal and the SIGTERM happen in parallel, so a preStop hook that sleeps about ten seconds gives the ALB and kube-proxy time to deregister the pod. Spring Boot's graceful shutdown then stops accepting new requests and lets in-flight ones finish within a phase timeout such as 30 seconds, and terminationGracePeriodSeconds must exceed the total. Align the ALB deregistration delay with it. At ShopNow, scaling from 60 to 24 pods under load produced 0.7% 502 and 504 errors before these fixes and 0.01% after.

Q6Senior EngineerTricky

Why are idempotency keys needed?

Idempotency keys are needed because in a scaled-out system a retry may land on a different instance from the original request, so no instance can rely on its own memory to recognise a duplicate. A client times out, retries the order placement, and without protection two orders or two payment captures are created. With an Idempotency-Key header, the first instance records the key and the result in a shared store, and any instance that receives the retry returns the stored result instead of repeating the side effect. Store keys with a time-to-live matching the retry window, and make the record and the business change atomic, for example in the same database transaction.

Q7Technical ArchitectDesign

How do you place instances for resilience?

Spread instances so that no single failure domain removes too many of them. Use topology spread constraints with the zone key and a maxSkew of one so replicas are balanced across three Availability Zones, and pod anti-affinity or a hostname spread so replicas do not share a node. A PodDisruptionBudget limits voluntary disruptions, such as node drains during upgrades or Karpenter consolidation, to, say, 10% of replicas at once. Size the fleet so that losing a whole zone still leaves enough capacity; ShopNow keeps an AZ reserve, which is why its checkout efficiency at 66 pods is 79%. Finally, run chaos tests that kill pods and simulate a zone loss under load.

Q8Technical ArchitectInternals

How do Kafka partitions limit scale-out?

Within a Kafka consumer group, each partition is assigned to exactly one consumer, so the number of active consumers can never exceed the number of partitions. Any extra consumer receives no assignment and sits idle while still costing memory and a slot. ShopNow scaled the inventory consumer group from 12 to 24 pods during a sale, but orders.events had 12 partitions, so half the pods did nothing and consumer lag kept growing. The fix is to provision partitions for future peak parallelism, here 48, ahead of time, because adding partitions later changes key-to-partition mapping and can break ordering for keyed events. Autoscalers such as KEDA should cap replicas at the partition count.

Q9Solution ArchitectDesign

How do you plan horizontal scaling for a whole system?

Plan it tier by tier, because each tier scales out in a different unit and has its own ceiling. For every tier, define the scale-out unit, the current range, the hard limit and the next step: checkout pods from 24 to 89 with a maximum of 110, search pods plus OpenSearch shards, Valkey shards from six to twelve, Aurora readers auto-scaling from two to five, Kafka partitions and consumers. Then budget what scale-out multiplies, especially connections through a proxy and memory per pod. Load test to measure scaling efficiency, find the next shared bottleneck and fix it before the sale, and check cloud quotas such as vCPU limits, subnet IPs and ALB targets.

Q10Solution ArchitectTricky

What hidden state commonly breaks horizontal scaling?

The common culprits are in-memory HTTP sessions, local rate-limit counters, files written to local disk, in-process caches used as a source of truth, and scheduled jobs that run on every pod. Each works on one instance and breaks silently on many. ShopNow's rate limiter kept counters in pod memory, so scaling from 20 to 60 pods tripled the effective limit and partners exceeded their quotas until counters moved to Valkey. A @Scheduled cleanup ran on all 66 pods and multiplied database load 66 times until it used ShedLock or a dedicated CronJob. Sticky sessions on the load balancer often hide such state, so audit for them before scaling out.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 8

Architecture Quality Attributes

Volume I · Topic 1.1

Quality Attributes

Quality Attributes & Architectural Thinking

Volume I · Topic 1.2

Why Quality Attributes Matter

Quality Attributes & Architectural Thinking

Volume I · Topic 1.3

Quality Attribute Categories

Quality Attributes & Architectural Thinking

Volume II · Topic 1.1

Performance

Performance & Capacity

Volume II · Topic 1.2

Latency Components

Performance & Capacity

Volume II · Topic 1.3

Tail Latency

Performance & Capacity

Volume III · Topic 1.1

Scalability

Scalability & Elasticity

Volume III · Topic 1.2

Vertical Scaling

Scalability & Elasticity

Volume IV · Topic 1.1

Availability Fundamentals

Availability, Reliability & Resilience

Volume IV · Topic 1.2

Availability Calculation

Availability, Reliability & Resilience

Volume IV · Topic 1.3

Nines of Availability

Availability, Reliability & Resilience

Volume V · Topic 1.1

Security Requirements

Security, Privacy & Compliance

Volume V · Topic 1.2

Security Architecture

Security, Privacy & Compliance

Volume V · Topic 1.3

Threat Modeling

Security, Privacy & Compliance

Volume VI · Topic 1.1

Maintainability

Maintainability, Modifiability & Testability

Volume VI · Topic 1.2

Technical Debt

Maintainability, Modifiability & Testability

Volume VI · Topic 1.3

Code Quality

Maintainability, Modifiability & Testability

Volume VII · Topic 1.1

What Is Operability?

Operability, Observability & Deployability

Volume VII · Topic 1.2

Operational Readiness

Operability, Observability & Deployability

Volume VII · Topic 1.3

Production Readiness Checklist

Operability, Observability & Deployability

Volume VIII · Topic 1.1

Why Evaluate Architecture?

Quality Attribute Trade-offs & Architecture Evaluation

Volume VIII · Topic 1.2

Architecture Evaluation Methods

Quality Attribute Trade-offs & Architecture Evaluation

Volume VIII · Topic 1.3

ATAM

Quality Attribute Trade-offs & Architecture Evaluation