Your two runtimes compared — Go (zoth) vs Node (traverse)
Concrete, code-level differences between the two backends you own — from startup to shutdown.
Know your own system
Interviewers who see Go and TypeScript/Node on a resume will ask you to compare them. You own both services — so answer from your code, not from blog posts.
Startup and wiring
Zoth cmd/api/main.go boots in strict order: load config (config.Load() — env → .env → defaults, with Validate() that fatally rejects a JWT secret under 32 bytes) → connect Postgres → run embedded goose migrations under pg_advisory_lock(72183921) (two replicas migrating concurrently can't interleave) → optionally connect RabbitMQ (failure is tolerated with a warning — the outbox keeps the backlog) → construct repositories → services → router with middleware → start expiry loop + outbox relay goroutines → srv.ListenAndServe.
Traverse src/index.ts: dotenv → express → middleware stack → route mounts → async boot (prisma.$connect(), initializeQueues() with a 30 s Redis readiness wait, initializeCronJobs()) → listen on 0.0.0.0 in production. Container start (Dockerfile → start.sh) is even stricter: embedded redis-server starts, then npx prisma migrate deploy — and the container refuses to boot on a failed/stale migration (exit 1). Different runtimes, same principle: fail fast at boot, degrade gracefully at runtime.
Graceful shutdown
- Zoth:
signal.Notify(SIGINT, SIGTERM)→srv.Shutdown(ctx)with 10 s to drain in-flight requests → OTel flush with 5 s → close publisher/consumer/DB. The K8s termination path depends on this (Day 44: probes + terminationGracePeriod). - Traverse: SIGTERM →
closeAllQueues()(BullMQ workers finish current jobs) →closeRedisConnections()→prisma.$disconnect()→shutDownOtel()→ exit 0. Also:deploy-remote.shhealth-checks/api/health/db20 times × 5 s afterdocker-compose up -d --build.
Type systems and data access
- Go + SQLC: you write raw SQL, SQLC generates typed Go structs — SQL is checked at compile time against the schema. Zero reflection, zero ORM magic.
emit_pointers_for_null_types, uuid overrides. - TS + Prisma: schema-first, generated client,
P2002error codes to catch inmapPrismaError. The tradeoff: Prisma gives you migrations + types from one source; SQLC gives you the exact SQL you'll run. Your honest line: "I picked the ecosystem default in each language and would reach for the other one (pgx/Drizzle) if Prisma's query layer became opaque."
Error philosophy
- Zoth: sentinel
AppErrors (pkg/errors— ErrNotFound/ErrForbidden/ErrConflict…) rendered into a nested{"error":{"code","message"}}envelope by middleware.AppError.Iscompares by code, so wrapped messages still match sentinels. - Traverse:
AppError(message, statusCode, {code, field})+mapPrismaError+SAFE_ERRORSallow-list, so raw Prisma text never leaks (full treatment Day 72).
Concurrency primitives used for real
| Need | Zoth (Go) | Traverse (Node) |
|---|---|---|
| background loop | time.Ticker goroutine + context | node-cron + BullMQ repeatable job |
| fan-out | goroutines per probe (health checks run concurrently) | Promise.all/allSettled |
| mutual exclusion | sync.Mutex (publisher, registry) | single-threaded loop + Redis ops |
| atomic claim | UPDATE ... WHERE status=$expected | FOR UPDATE SKIP LOCKED |
Performance reality check
Go compiles to a static ~15 MB binary, boots in ms, no GC tuning needed at this scale. Node needs the runtime + node_modules but wins on iteration speed and the huge extension/iOS-adjacent ecosystem. Your summary line for interviews: "Go for the identity-control-plane where I wanted explicit SQL, tiny footprint, and static deployment to a 1-node cluster; Node for traverse because the whole product surface (extension, web, tooling) is TypeScript — one language across the stack beat runtime micro-optimization."
Bulletproof your resume
Say these answers aloud before moving on
These are the follow-ups an interviewer will actually ask about your projects.
Why is Go the right choice for zoth specifically?
Static binary into a distroless-ish alpine image (two-stage Dockerfile, CGO_ENABLED=0) keeps the image tiny for ACR push/pull on a cheap node; goroutines made the expiry loop, outbox relay, and consumer dispatch trivial; SQLC + Go's type system kept hand-written SQL the source of truth; and the 15/15s read/write timeouts plus signal-based graceful shutdown map directly onto Kubernetes lifecycle. None of these are impossible in Node — but each was cheaper in Go for a control plane.
Your traverse container runs migrations on every boot and exits if they fail. Why?
Because a stale schema serving traffic is worse than downtime: code compiled against columns that don't exist fails at runtime in unpredictable places. Failing the boot makes the deploy visibly broken (docker-compose health checks / deploy script fails) instead of silently wrong. The counterpart in zoth is migrations under pg_advisory_lock at API startup — same principle, different mechanism (zoth can't exit because K8s restarts it anyway; instead migration failure is fatal before ListenAndServe).
How do the two stacks handle a SIGTERM during in-flight work?
Zoth: http.Server.Shutdown drains requests up to 10 s, OTel flushes within 5 s, publisher/consumer/DB close in defer. Traverse: BullMQ workers close (current job finishes), Redis connections close, Prisma disconnects, OTel shuts down. In both, anything that exceeds the deadline is picked up after restart by the durability layer — outbox relay in zoth, at-least-once redelivery + idempotency in traverse. Shutdown correctness is really a recovery story.