Split API and jobs entrypoints — one service, two binaries' worth of main()
Split API and jobs entrypoints — one service, two binaries' worth of main()
A service ships two entrypoints from one module tree: cmd/server (HTTP API, long-running or Lambda-adapted) and cmd/jobs (scheduled/batch work: migrations, dunning runs, report generation). Both wire the same internal/ packages; only main() differs.
Problem it solves
- Mixing scheduled work into the API process couples deploys to job schedules, blocks rolling restarts on long jobs, and makes Lambda adaptation impossible (jobs don't fit request/response). One binary doing both forces the worst lifecycle on each.
Shape
cmd/server/main.go— HTTP surface only: router, handlers, graceful shutdown.cmd/jobs/main.go— subcommand-style CLI:jobs daily [--as-of DATE] [--no-batch],jobs migrate— each job is a one-shot, idempotent function over the same store interfaces.- Shared
internal/packages (config, db, domain services) — no job logic in the API package, no HTTP types in jobs. - Python equivalent: one FastAPI app object (lifespan: migrate, seed, close pool) — uvicorn serves it, mangum adapts it to Lambda; jobs stay separate scripts.
- Scheduling is external (EventBridge/cron hitting the jobs entrypoint or queue), never in-process timers.
Rules
- Jobs are one-shot and idempotent — safe to re-run, safe to retry, safe on Lambda scheduled events.
- Jobs take explicit time parameters (
--as-of) for deterministic backfills and tests. - Config via env only; store adapters env-selected (same as API).
- API startup never runs jobs; job startup never binds a port.
Anti-patterns
- Cron goroutines inside the API server (frozen on Lambda, double-run on multi-instance).
- Jobs writing directly to vendor APIs without the outbox (
transactional-outbox). - Duplicated business logic between API handlers and job scripts — both call the same service layer.
- Long jobs holding DB transactions open — batch and commit incrementally.
Sources
- repayments/backend-go/cmd/server/main.go vs cmd/jobs/main.go
- repayments/backend/app/main.py (FastAPI lifespan: migrate, ensure_default_rules, close_pool) + backend/Dockerfile
- factory/AGENTS.md (entry points rule)