Back to Scaling, Load Balancing & Performance
Q12Question 12 of 20

How do you scale a microservices application

Key Concepts
  • The defining advantage of microservices is independent scaling: scale only the service that is hot, not the whole system.
  • Stateless services scale horizontally behind load balancers; the API gateway handles routing, auth, and rate limiting at the edge.
  • Kubernetes automates scaling with Deployments, HPA for CPU-based scaling, and KEDA for event-driven scaling.
  • Database scaling is the hardest part: connection pooling, caching in Redis, read replicas, then sharding when a single primary cannot keep up.
  • Asynchronous messaging via Kafka decouples services and absorbs traffic spikes; consumers scale on queue lag.
  • Monitoring tells you which service is the current bottleneck so you scale the right thing, not everything.
  • Circuit breakers and bulkheads ensure that when one overloaded service fails, callers degrade gracefully instead of cascading.

Definition

Scaling microservices means scaling each service independently: stateless replicas behind load balancers, an API gateway in front, containers auto-scaled by Kubernetes, the database scaled with caching and replicas, slow work offloaded asynchronously, all guided by monitoring.
  • Independent scaling – Each service scales according to its own load, not the whole system as one block.
  • Load balancing – Distributes requests across replicas and routes around unhealthy ones.
  • API gateway – The single entry point that handles routing, auth, rate limiting, and request aggregation.
  • Database scaling – Connection pooling, caching, read replicas, and sharding when a single primary cannot keep up.
  • Async messaging – Kafka or RabbitMQ decouple services and absorb spikes; consumers scale on queue lag.
  • Auto-scaling – Kubernetes HPA for CPU/memory, KEDA for event-driven scaling, Cluster Autoscaler for adding nodes.

Imagine the National Bank of India is a set of specialized departments: transfers, loans, statements, fraud checks. Scaling the bank does not mean cloning the entire building. It means noticing that the transfer department has the longest queue today and opening more transfer counters specifically, while the loans department stays at its usual size. A receptionist at the door routes customers, each department opens or closes counters as its queue changes, and slow paperwork is handed off to a back office.

Understanding the Concepts

Why is independent scaling the key advantage?

In microservices, each service is a separate deployable unit. You scale only the service that is hot. If transfers spike but loan applications are quiet, you add transfer replicas and leave the loan service alone.

JAVA
package com.bank.scaling;

public class IndependentScaling {
    public static void main(String[] args) {
        System.out.println("Monolith scaling: scale EVERYTHING even if only transfers are hot");
        System.out.println("  3 monolith replicas x 16 GB = 48 GB total");
        System.out.println();
        System.out.println("Microservice scaling: scale ONLY what is hot");
        System.out.println("  transfer-service: 5 replicas x 2 GB = 10 GB");
        System.out.println("  loan-service: 1 replica x 2 GB = 2 GB");
        System.out.println("  statement-service: 2 replicas x 2 GB = 4 GB");
        System.out.println("  Total: 16 GB (vs 48 GB with monolith)");
    }
}
TEXT
Expected output:
Monolith scaling: scale EVERYTHING even if only transfers are hot
  3 monolith replicas x 16 GB = 48 GB total

Microservice scaling: scale ONLY what is hot
  transfer-service: 5 replicas x 2 GB = 10 GB
  loan-service: 1 replica x 2 GB = 2 GB
  statement-service: 2 replicas x 2 GB = 4 GB
  Total: 16 GB (vs 48 GB with monolith)

How does the API gateway protect the system?

The gateway is the single entry point handling routing, authentication, and rate limiting. Rate limiting at the gateway is itself a scaling tool: it sheds excess load before it reaches services.

YAML
spring:
  cloud:
    gateway:
      routes:
        - id: transfer-route
          uri: lb://transfer-service
          predicates:
            - Path=/api/v1/transfers/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200
        - id: loan-route
          uri: lb://loan-service
          predicates:
            - Path=/api/v1/loans/**
TEXT
Gateway responsibilities:
  1. Route /transfers to transfer-service, /loans to loan-service
  2. Validate JWT at the edge
  3. Rate limit: 100 req/sec sustained, 200 burst per user
  4. Shed excess load BEFORE it reaches services

How does Kubernetes automate container scaling?

Each service is a container image, Kubernetes runs declared replicas, and the HPA adjusts the count based on metrics.

YAML
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: transfer-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: transfer-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
JAVA
package com.bank.scaling;

public class K8sAutoScaling {
    public static void main(String[] args) {
        System.out.println("Auto-scaling layers:");
        System.out.println("  HPA: scales pod count based on CPU or custom metrics");
        System.out.println("  KEDA: scales consumers on Kafka consumer lag");
        System.out.println("  Cluster Autoscaler: adds nodes when no room for new pods");
    }
}
TEXT
Expected output:
Auto-scaling layers:
  HPA: scales pod count based on CPU or custom metrics
  KEDA: scales consumers on Kafka consumer lag
  Cluster Autoscaler: adds nodes when no room for new pods

Why is database scaling the hardest part?

Scaling the app tier tenfold multiplies pressure on a single database. The techniques, in order, are pooling, caching, read replicas, and sharding.

JAVA
package com.bank.scaling;

public class DatabaseScaling {
    public static void main(String[] args) {
        System.out.println("Database scaling progression:");
        System.out.println("  1. Connection pooling (HikariCP): don't open a conn per request");
        System.out.println("  2. Redis cache: serve hot reads, shield the DB");
        System.out.println("  3. Read replicas: heavy reads go to replicas, writes to primary");
        System.out.println("  4. Sharding: split data by account_id across multiple DBs");
        System.out.println();
        System.out.println("Scaling 10x app instances without DB scaling -> DB is the bottleneck");
    }
}
TEXT
Expected output:
Database scaling progression:
  1. Connection pooling (HikariCP): don't open a conn per request
  2. Redis cache: serve hot reads, shield the DB
  3. Read replicas: heavy reads go to replicas, writes to primary
  4. Sharding: split data by account_id across multiple DBs

Scaling 10x app instances without DB scaling -> DB is the bottleneck

How does async messaging help with scaling?

Instead of synchronous chains, a service does its critical step and publishes an event to Kafka. The queue absorbs spikes and consumers scale independently.

JAVA
package com.bank.scaling;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Service
public class AsyncTransferService {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public AsyncTransferService(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public String initiateTransfer(String from, String to, BigDecimal amount) {
        // Do the critical step (debit) synchronously
        // Then publish event for downstream processing
        String event = "{\"from\":\"" + from + "\",\"to\":\"" + to
                + "\",\"amount\":" + amount + "}";
        kafkaTemplate.send("transfer-events", from, event);
        System.out.println("Transfer event published, consumers process at their pace");
        return "ACCEPTED";
    }
}
TEXT
Expected output:
Transfer event published, consumers process at their pace

Kafka as shock absorber:
  Producer: 10,000 events/sec during salary batch
  Consumer: processes at 2,000 events/sec
  Queue absorbs the spike, consumer drains steadily
  KEDA scales consumer pods on lag

How does monitoring guide scaling decisions?

Monitoring tells you which service is the current bottleneck so you scale the right thing rather than everything.

JAVA
package com.bank.scaling;

public class MonitoringGuidedScaling {
    public static void main(String[] args) {
        System.out.println("Scaling principle: measure, find bottleneck, scale THAT");
        System.out.println();
        System.out.println("Metrics to watch:");
        System.out.println("  Per-service: CPU, memory, request rate, error rate, p99 latency");
        System.out.println("  Database: connection pool usage, query time, replication lag");
        System.out.println("  Kafka: consumer lag, partition count");
        System.out.println("  Kubernetes: pod count, HPA scaling events, node utilization");
        System.out.println();
        System.out.println("Example: p99 latency on transfer-service spikes");
        System.out.println("  -> Check CPU: 90% -> scale transfer-service pods");
        System.out.println("  -> Check DB: query time 500ms -> add read replica or index");
    }
}
TEXT
Expected output:
Scaling principle: measure, find bottleneck, scale THAT

Metrics to watch:
  Per-service: CPU, memory, request rate, error rate, p99 latency
  Database: connection pool usage, query time, replication lag
  Kafka: consumer lag, partition count
  Kubernetes: pod count, HPA scaling events, node utilization

How do circuit breakers prevent cascade during scaling events?

When one service is overloaded, circuit breakers ensure callers fail fast and degrade gracefully instead of piling up.

JAVA
package com.bank.scaling;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;

@Service
public class StatementClient {

    @CircuitBreaker(name = "statement", fallbackMethod = "fallback")
    public String getStatement(String accountId) {
        return statementService.fetch(accountId);
    }

    public String fallback(String accountId, Throwable t) {
        return "Statement temporarily unavailable. Please try again later.";
    }
}
TEXT
Scenario:
  Statement service overloaded during month-end
  Circuit breaker opens -> callers get fallback instantly
  Transfer service continues working unaffected
  Statement service recovers -> circuit closes -> normal flow resumes

What is the guiding principle for scaling microservices?

Measure, find the bottleneck, scale that one service or tier, and repeat.

TEXT
Scaling playbook:
  1. Measure: Prometheus + Grafana dashboards
  2. Find bottleneck: which service has highest p99 or CPU?
  3. Scale THAT service: increase HPA max or add resources
  4. Measure again: did the bottleneck shift?
  5. Repeat: scaling is continuous, not one-time

Visual Overview

DIAGRAM
  Microservice Scaling Architecture:

                         ┌─────────────┐
  Clients ──────────────▶│ API Gateway │
                         │ (route, auth│
                         │  rate limit)│
                         └──┬──────┬───┘
                            │      │
              ┌─────────────┘      └─────────────┐
              ▼                                  ▼
  ┌───────────────────┐              ┌───────────────────┐
  │ transfer-service  │              │ statement-service  │
  │ 5 replicas (HPA)  │              │ 2 replicas (HPA)  │
  │ Circuit Breaker   │              │ Circuit Breaker   │
  └─────────┬─────────┘              └─────────┬─────────┘
            │                                  │
            ▼                                  ▼
  ┌───────────────────┐              ┌───────────────────┐
  │ Redis Cache       │              │ Read Replica      │
  │ (hot balance      │              │ (statement        │
  │  lookups)         │              │  queries)         │
  └───────────────────┘              └───────────────────┘
            │
            ▼
  ┌───────────────────┐
  │ Primary DB        │
  │ (writes only)     │
  └───────────────────┘
            │
            ▼
  ┌───────────────────┐
  │ Kafka             │
  │ (async events,    │
  │  decoupling)      │
  └───────────────────┘

Key Takeaway

The defining advantage when scaling a microservices application is that you scale each service independently. If transfers spike during a festival but loan applications are quiet, you add transfer replicas and leave the loan service alone. On the request path, the primary lever is horizontal scaling of stateless services behind load balancers, with an API gateway at the edge for routing, authentication, and rate limiting.

The hardest part is almost always the data path. Scaling the app tier tenfold multiplies pressure on one database, so connection pooling, caching in Redis, read replicas, and eventually sharding are essential, not optional. Asynchronous messaging via Kafka decouples services and absorbs spikes: a transfer does its critical step and publishes an event, then returns fast, while downstream consumers for statements and notifications process at their own pace and scale on queue lag.

At National Bank of India, Pankaj followed this playbook: Prometheus dashboards showed the transfer service's CPU spiking during month-end, so the HPA scaled it from 2 to 8 pods. When the database became the bottleneck, they added a Redis cache for balance lookups that cut DB load by 80%. When the statement path overloaded, its circuit breaker opened and transfers kept flowing. The guiding principle is: measure, find the bottleneck, scale that one service or tier, and repeat.

Remember one sentence: scaling microservices means independently growing each service's capacity guided by monitoring, with stateless replicas behind load balancers, a gateway at the edge, the database scaled with caching and replicas, and async messaging to decouple and absorb spikes.

---

Share
Use ← → arrow keys to navigate