Day 9 of 100·System Design Day·497 words

Horizontal vs vertical scaling; monolith vs microservices

The decision your repos already made differently on purpose — and how to defend both.

Concept → your projects

Vertical scaling = bigger machine (more CPU/RAM on one box). Horizontal = more machines. The rule of thumb:

  • Vertical: simple, zero code change, hits a ceiling (~128–256 cores, RAM), is a SPOF, downtime on resize.
  • Horizontal: needs statelessness (or sharded state), load balancing, and answers the availability NFR (replicas fail independently) — but introduces distributed-systems problems: consistency, coordination, deployment complexity.

Monolith vs microservices is a different axis but the same question: how many independently-deployable units, and where do the boundaries fall? Microservices buy: independent scaling (the 1-heavy-service problem), failure isolation, team ownership, tech heterogeneity. They cost: network calls instead of function calls (partial failures!), distributed tracing, eventual consistency, and — the one juniors forget — operational surface (N × deploy, monitor, secure, upgrade).

Your two answers

Zoth: 3 microservices on 1 node. zoth-api, zoth-emitter, zoth-alerting share one Postgres and one RabbitMQ. Why split at all if they run on one machine?

  1. Failure isolation of the risky part. The emitter talks to external SaaS APIs with retries, 5-minute handler timeouts, DLQs. A GitHub outage (or a bug in a connector) must never take down request handling. Process isolation is the cheapest container (pun intended) for that.
  2. Independent deploy cadence. Connectors change often (zlink); policy engine rarely.
  3. Event-driven decoupling was the design center — once provisioning is async via RabbitMQ, the consumer naturally becomes its own runtime.

But the honest scaling note: all three run as single replicas — this is modularity, not scale-out. replicas: 1 in the prod overlay. If you need HA: 3 api replicas behind the Service (already stateless — JWT is stateless, no sticky sessions needed), 2+ emitters (safe: dedup via processed_events, prefetch spreads load), alerting needs claiming (SKIP LOCKED-style) to avoid double-detection.

Traverse: 1 deployable + queues. One Express process, one Postgres, one Redis. Why no split?

  • The workload is one domain (submissions → schedule → notify) with low QPS; the "microservice boundary" would save nothing.
  • The async decoupling that microservices would provide is achieved in-process with BullMQ workers — you got isolation of slow work without network hops.
  • Ops budget: 1 VPS, docker-compose, GitLab CI. A service mesh here would be comedy.

That's the senior summary: zoth split processes because failure domains and deploy cadence differed; traverse didn't because they didn't. Neither decided based on "microservices are modern."

The in-between most companies actually run

A modular monolith: one deployable, strictly layered modules (zoth's internal/{controllers,services,repositories} and traverse's routes→controllers→services→lib are exactly this). You extract a service when a module needs different scaling/deploy/failure characteristics — zoth extracted emitter; traverse hasn't needed to. Write that sentence down; it's the whole topic in one line.

Practice

"Design a rate limiter serving 100k rps" — horizontally scale stateless limiter nodes + Redis for counters; "design a batch report generator" — vertical for the DB, horizontal workers with SKIP LOCKED. The answer follows the workload shape, always.

Bulletproof your resume

Say these answers aloud before moving on

These are the follow-ups an interviewer will actually ask about your projects.

01

Zoth is called microservices but runs single replicas on one node. Isn't that just a monolith with extra steps?

It's a distributed failure-isolation architecture, not a scale-out one. The split buys: (1) the emitter's external-API risk (timeouts, retries, DLQ) can't crash request handling; (2) independent deploys — connectors iterate fast, the policy engine rarely; (3) each service scales on its own axis later (emitter replicas are safe today thanks to event dedup). What it deliberately does not buy yet is availability — that would need multiple replicas per service and a multi-node cluster, which the $30 budget didn't justify. Scale for requirements, not for diagrams.

02

When would you extract traverse's AI analysis into its own service?

Three triggers: (1) CPU/memory profile diverges — if diff computation or local models start starving the API loop; (2) deploy cadence — if prompt/model changes need shipping without touching the API; (3) security boundary — if BYOK keys need an isolated blast radius. Today the BullMQ worker already isolates it in-process, which is 80% of the benefit at 5% of the ops cost.

03

What's the hardest true cost of microservices that people underestimate?

Partial failure. Once a call crosses the network, it can be slow, dropped, or duplicated — so you need timeouts, retries with idempotency, and backpressure everywhere, plus tracing to debug it. Zoth's outbox + dedup + idempotent revoke exist precisely because I accepted that cost knowingly; a monolith gets function-call semantics for free.