Chapter 114 โ€” Scalable APIs

๐Ÿ“– Definition

A scalable API is one whose throughput and latency degrade gracefully as users, data, or traffic grow.

๐Ÿ” Layers of Scaling

plaintext
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Clients (web / mobile)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           CDN            โ”‚  โ† cache static + cacheable API responses
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚      Load Balancer       โ”‚  โ† round-robin / least-conn
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  API Gateway (optional)  โ”‚  โ† auth, rate-limit, routing
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Stateless API Instances โ”‚  โ† horizontally scalable
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
       โ–ผ              โ–ผ
 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 โ”‚  Cache   โ”‚   โ”‚   Queue    โ”‚
 โ”‚  (Redis) โ”‚   โ”‚ (BullMQ)   โ”‚
 โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ–ผ             โ–ผ
 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 โ”‚     Database (Mongo)     โ”‚
 โ”‚  Replicas, Sharding      โ”‚
 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ’ป Code Example โ€” Statelessness

Stateless services scale horizontally. Avoid:

  • In-process session storage.
  • Local file uploads.
  • Long-lived WebSocket connections without sticky routing.
// โŒ in-process map โ€” won't survive a restart, not shared across instances
const sessions = new Map();

// โœ… Redis โ€” shared, durable
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
await redis.set(`session:${id}`, JSON.stringify(data), { EX: 3600 });

๐Ÿš€ Caching Strategies

Layer What to cache TTL
Browser Static assets, GET responses with Cache-Control hours / days
CDN Same as above; geo-distributed hours / days
App-process (lru-cache) Hot lookups inside one instance seconds / minutes
Redis Shared cache for multi-instance seconds / minutes / hours
DB query cache Materialized views, repeated heavy aggregates hours

Cache-Aside (most common)

async function getUser(id) {
  const key = `user:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const user = await User.findById(id).lean();
  if (user) await redis.set(key, JSON.stringify(user), { EX: 300 });
  return user;
}

// Invalidate on writes
async function updateUser(id, patch) {
  const user = await User.findByIdAndUpdate(id, patch, { new: true });
  await redis.del(`user:${id}`);
  return user;
}

๐Ÿ’ป Code Example โ€” Queue / Background Jobs

Anything that's slow, retryable, or doesn't need a sync response โ†’ queue it.

// producer (in API)
import { Queue } from "bullmq";
const emailQ = new Queue("email", { connection: { host: "redis" } });

app.post("/users", async (req, res) => {
  const user = await UserService.create(req.body);
  await emailQ.add("welcome", { to: user.email });   // returns immediately
  res.status(201).json(user);
});

// worker (separate process)
import { Worker } from "bullmq";
new Worker("email", async (job) => {
  await sendEmail(job.data.to, "Welcome!");
}, { connection: { host: "redis" } });

Common use cases:

  • Email / SMS / push notifications.
  • Image resizing.
  • Bulk imports/exports.
  • Webhook delivery (with retries).

๐Ÿ’ป Code Example โ€” Database Read Replicas

// In mongoose, use readPreference for analytics queries
Order.find({})
  .read("secondaryPreferred")    // read from replica
  .lean();

๐Ÿ’ป Code Example โ€” Pagination & Filtering at Scale

// Cursor pagination (Chapter 36) โ€” required at scale
GET /messages?after=<id>&limit=20

Always:

  • Index the cursor field.
  • Cap limit server-side (no ?limit=1000000).
  • Avoid count for huge collections (use approximations).

๐Ÿ’ป Code Example โ€” Connection Pooling

mongoose.connect(url, {
  maxPoolSize: 50,           // tune based on load tests
  minPoolSize: 5,
  serverSelectionTimeoutMS: 5000,
});

For Postgres, use pg-pool or PgBouncer in front.

๐Ÿ’ป Code Example โ€” Observability

import pino from "pino";
const logger = pino({ level: "info" });

// Structured logs
logger.info({ userId: user.id, route: req.url }, "Request handled");

// Metrics with prom-client
import client from "prom-client";
const reqDuration = new client.Histogram({
  name: "http_request_duration_ms",
  help: "Request duration in ms",
  labelNames: ["method", "route", "status"],
});

app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () =>
    reqDuration.labels(req.method, req.route?.path || "", res.statusCode)
               .observe(Date.now() - start)
  );
  next();
});

// Expose /metrics for Prometheus scraping
app.get("/metrics", async (req, res) => {
  res.set("Content-Type", client.register.contentType);
  res.end(await client.register.metrics());
});

Pair with OpenTelemetry for distributed tracing across services.

๐Ÿ’ป Code Example โ€” Graceful Shutdown

const server = app.listen(port);

const shutdown = async () => {
  console.log("Shutting down");
  server.close();                  // stop accepting new requests
  await mongoose.disconnect();
  await redis.quit();
  process.exit(0);
};

process.on("SIGTERM", shutdown);
process.on("SIGINT",  shutdown);

๐Ÿ“ˆ Scaling Patterns Summary

Symptom Solution
One DB row is hot Cache (Redis), denormalize
Same query repeated Cache results
CPU-bound endpoint Offload to worker thread or queue
Slow third-party call Async queue + webhook callback
10k connections per instance Add instances behind LB
One DB is saturated Add read replicas / shard
Static assets slow CDN
Cold starts Pre-warm or keep min instances

๐ŸŽฏ Likely Interview Questions

  1. How would you scale this API to 10ร— traffic?
  2. Where would you add caching?
  3. When would you introduce a queue?
  4. How do you keep services stateless?
  5. What metrics matter for a healthy API? โ€” Request rate (RPS), error rate, latency (p50, p95, p99), saturation (CPU, memory, DB connections). The RED method (Rate, Errors, Duration).

โ† Authentication | Index | Next: Caching โ†’