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?
- 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.
- Independent deploy cadence. Connectors change often (zlink); policy engine rarely.
- 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.
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.
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.
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.