Our service generator: scaffolding a Go microservice in twelve minutes
We built a CLI that turns a Ruby schema file into a complete production microservice — Go code, Helm chart, ArgoCD manifest, Ansible playbook. Here is what it does, how it works, and why we would not build it for everyone.
Most of our services look alike. They have CRUD endpoints over a domain entity. They sit behind an OIDC layer. They emit Prometheus metrics. They land in a Helm chart that ships through GitLab CI to an ArgoCD-managed K3s cluster. The shape repeats; the entity does not.
A few iterations into building that pattern by hand, we did what engineers usually do: we wrote a generator. This is what it does, how it is structured, and the trade-off we accepted to keep it useful.
The input
The generator takes a Ruby schema file as input. We picked Ruby’s Active Record create_table syntax for one reason: it is the most expressive minimum we know for “here are the columns of a table”. You can write one in your head:
# schema_bar.rb
create_table "bars" do |t|
t.string "name", null: false
t.string "address"
t.float "latitude"
t.float "longitude"
t.string "owner_id" # server-set from JWT sub
t.datetime "opened_at"
t.timestamps
end
add_index "bars", ["owner_id", "name"], unique: true
Six columns, one compound index. That is the input.
The thirteen-step pipeline
Running
go run main.go --schema schema_bar.rb --service-name bar-service --enable-keycloak
gives us a complete, working service. The CLI walks a thirteen-step pipeline. Each step is small and predictable. From top to bottom:
- Parse the schema — a regex-based Ruby reader pulls out columns, types, and compound indexes. Unknown types fall back to
stringwith a warning. - Cleanup — the staging output directory gets wiped so nothing from a previous run leaks through.
- Render Go code — model, repository, service, handler, main, mongo, config, rate_limiter, health_handler. Each template gets the parsed schema and our project-wide conventions.
- Scaffold non-code files —
go.mod,Dockerfile,docker-compose.yml,.gitlab-ci.yml,.env.example,.gitignore. - Move the output to its final repo location (a sibling workspace under
~/Work/). - Render the Helm chart —
Chart.yaml,values.yaml,values-dev.yml, templates for the deployment, service, ingress, HPA, ServiceMonitor, and a Grafana dashboard. Output goes to the right helm-resources repo. - Render the ArgoCD
Applicationmanifest into the appropriatetreeformation/location. - Render the deploy playbook — a single-file Ansible playbook that creates the namespace, drops any required env Secret, applies the ArgoCD Application, and waits for the sync to go green.
- Optional: render a Keycloak realm-import if
--enable-keycloakwas passed. - Wire monitoring — ServiceMonitor + Grafana dashboard get included when
--enable-monitoringis set (the default). - Write the README with a curl example, a deploy command, and the secrets to rotate.
- Run
go mod tidyso the new module is ready to compile. - Print a summary with each file that was created, where it lives, and the next manual step.
End to end, the run takes about three seconds. The “twelve minutes” in the title is the time from “I want a new service that owns Things” to “Things is running in production with TLS at a public domain”. The bulk of those twelve minutes is the merge + ArgoCD reconciliation, not the generator.
What conventions look like when they are templated
This is the bit that pays off. Because every service comes out of the same templates, we know without checking that every service:
- Listens on the same default port pattern
- Has the same
/health,/ready,/metricsendpoints - Logs structured JSON with the same trace-id propagation
- Has the same Dockerfile shape (multi-stage Go build → distroless)
- Uses the same CI shared component for build + image-tag bump
- Has the same chart skeleton (deployment, service, ingress, HPA, ServiceMonitor)
- Has the same OIDC middleware when
--enable-keycloakis on - Has the same Grafana dashboard, with the panel layout already correct
When a service breaks at 3am, we are looking at the same shape we are familiar with from the last five services. Investigating becomes pattern-recognition instead of archaeology.
The trade-off we accepted
Generators have one well-known cost: regeneration drift. If we hand-tune a generated file and then re-run the generator, the hand edits get wiped.
We accepted that cost by being explicit about it. The generator output is non-destructive on regeneration: hand-extras under internal/services/*_extra.go, anything in cmd/<other>/, and most scaffold files (.env, Dockerfile, .gitlab-ci.yml) are skipped when they already exist. The generator templates themselves are the source of truth for the generated files; for hand-tuned files the source of truth is the hand-tuned file.
That contract has held up across several services. One service has drifted far enough from a clean regeneration that we marked it explicitly in our agent notes: do not regenerate bar-service, add new entities by hand. That is the right answer for a service that has been hand-tuned for a year.
When we would not build a generator
To be honest about what this tool is good for:
- Generators are right when you ship many services that share a shape and the shape changes more slowly than the services do. Five services that each took a week to build will pay back a one-week investment in the generator within a quarter.
- Generators are wrong for one-off shapes. If you have a single API service and you are not planning a second one, the generator is overhead. Write the service by hand.
- Generators are wrong when the shape is not yet stable. You cannot template what you have not converged on. Build three services by hand first; the third one will tell you what to template.
The maram generator was built after we had shipped three CRUD services by hand and we were tired of writing the same Helm + ArgoCD + Ansible boilerplate again. By that point the shape was stable enough to compress.
The lesson
Code generation is leverage, not magic. The leverage is real when the shape is stable and the cost of repeating it is real. If you have an engineering team paying that cost without acknowledging it — Helm boilerplate copy-pasted across services, Dockerfile drift, ArgoCD manifests that should be identical but quietly diverge — you have a generator opportunity.
If you are not sure whether your shape is stable enough yet, the cheapest move is to ship one more service by hand and pay attention to what you copied from the last one. The repeated bits are your template.
If you want help figuring that out — or you want our generator pointed at your stack — we are an email away.