Skip to main content

Golden Paths: How Backstage Templates Guarantee Correct-by-Default Services

· 5 min read
Platform Engineering Team

Documentation gets ignored. Runbooks get outdated. Wiki pages accumulate conflicting instructions from three different authors. When the "right way" to create a service exists only in documentation, every new service is a chance to deviate from convention.

Backstage Scaffolder templates change this equation. Instead of documenting the correct path and hoping engineers follow it, we encode the correct path into a form that produces correct output every time. This post is about how our template chain works, why the dependency order matters, and what happens when you click Create.

The problem with documentation-driven standards

Our platform convention defines a precise naming pattern, 9 required labels, security defaults, and a specific repository structure. Before templates, applying these standards meant:

  1. Reading 3 different wiki pages
  2. Copy-pasting YAML from examples (often outdated)
  3. Hoping the reviewer catches what you missed
  4. Waiting for the platform team to fix what the reviewer missed

The result: 80% of new services needed at least one correction PR after the initial creation. Convention compliance was aspirational.

Templates as enforcement

A Backstage Scaffolder template is a YAML definition that combines a user-facing form with a sequence of automated actions. The developer fills in the form. The template generates all the files, opens the PRs, and registers the entity in the catalog.

The template does not document the convention — it implements the convention. There is no way to create a service with missing labels, wrong naming, or incorrect security context, because the template generates all of those.

The template chain

Templates have dependencies. You cannot create a service without a system. You cannot create a system without a domain. The chain enforces this order:

People layer (run any time)
──────────────────────────────────────────────────────
create-group → Group entity + RBAC + ArgoCD roles
create-user → User entity + RBAC + ArgoCD access

Platform layer (run in order)
──────────────────────────────────────────────────────
create-domain
│ Domain entity, {domain}-gitops repo, AppProject

create-system
│ System entity, ApplicationSet

├─────────────────────────┬─────────────────────┐
▼ ▼ ▼
create-service create-resource create-secret
Component entity, Resource entity, SealedSecret
App Repo (CI/CD, Crossplane Claim/env, manifest/env
TechDocs, .k8s), infra namespace
k8s manifests/env

Each template uses EntityPicker to select its parent. You pick the domain from a dropdown that only shows existing domains. You pick the system from a dropdown filtered to that domain's systems. Referential integrity is enforced by the UI itself.

What create-service actually does

When a product engineer runs create-service, they fill in:

  • System: selected from catalog (domain, repo, AppProject resolve automatically)
  • Service name: e.g., api, worker, frontend
  • Service type: api / worker / frontend / grpc / cronjob
  • Container image: registry path
  • Resource profile: which CPU/memory tier
  • Target environments: which envs to deploy to

Then the template executes:

Step 1 — Generate the Application Repository

A new repository is created with:

{app}-repo/
├── src/ ← boilerplate source code
├── Dockerfile ← optimized, non-root, language-specific
├── .github/workflows/
│ └── ci.yaml ← lint, test, build, publish, tag update
├── .k8s/
│ └── values.yaml ← overrides (image tag, replicas, resources)
├── docs/
│ └── index.md ← TechDocs base
└── catalog-info.yaml ← Backstage Component entity

Step 2 — Generate Kubernetes manifests in the domain repo

In {domain}-gitops/k8s/{env}/{service}/, the template creates:

  • Namespace with all 9 required labels
  • Deployment with security context, resource limits, readiness/liveness probes
  • Service exposing the correct port
  • HPA with env-appropriate scaling (dev: 1→2, staging: 2→5, prod: 3→10)
  • PDB (prod only, minAvailable: 1)
  • NetworkPolicy isolating by project boundary

Step 3 — Update the ApplicationSet

In platform-gitops, the template adds the new service as an element in the existing ApplicationSet:

elements:
- service: api # ← existing
- service: worker # ← added by template

Step 4 — Register and open PRs

The template opens two PRs:

  1. Domain repo PR: k8s manifests + Component entity
  2. Platform repo PR: ApplicationSet element

It also registers the new catalog-info.yaml in the Backstage catalog.

Merge order matters: Platform PR first (ApplicationSet must exist before ArgoCD can sync), then domain PR. Within minutes of both merging, the service is running on the dev cluster.

The two-PR workflow

Every template follows the same pattern: one PR on the domain/catalog repo, one PR on platform-gitops. This separation exists because:

  • Domain teams own their manifests — they review and merge what they deploy
  • Platform team owns the routing — they review changes to ArgoCD config and RBAC
  • CODEOWNERS enforce this — wrong-repo changes get blocked automatically

Results

Since adopting templates:

  • 100% label compliance on all new services (was < 20%)
  • < 3 minutes from clicking Create to PRs opened (was hours of manual YAML)
  • Zero convention violations on template-generated services
  • Service running in dev in < 30 minutes (was 2–5 days)

The templates did not just make service creation faster — they made convention violations structurally impossible for new services. The "correct way" is the only way.


The full template chain and output summary are documented in the Platform Convention — Templates. The Backstage entity model and catalog design are in the Backstage Convention.