Self-Service Developer Platforms: Golden Paths and Backstage
Self-Service Developer Platforms: Golden Paths and Backstage
Twenty-one chapters of pipeline configuration. Security gates, performance budgets, GitOps, progressive delivery. A new developer joins the team. They need to create a new service. They spend three days copying YAML from the checkout service, renaming things, and missing half the configuration.
A golden path is the paved road. A template repository with the CI pipeline, Dockerfile, Kubernetes manifests, security scans, and monitoring already configured. The developer creates a new service in 10 minutes and gets all 21 chapters of pipeline maturity for free.
The Failure
The team created five services over two years. Each service had a slightly different pipeline. The checkout service used Trivy; the catalog service used Snyk. The payments service had canary deployments; the inventory service had rolling updates. The frontend had performance budgets; the backend services did not. Every service was a snowflake.
When a security policy changed (e.g., “all services must use Trivy”), someone had to update five repositories individually. Two were missed. The golden path eliminates snowflakes by starting every service from the same template.
The Mechanism
Golden Path Components
| Component | Template Contents |
|---|---|
| CI Pipeline | .github/workflows/ci.yml with all gates |
| Dockerfile | Multi-stage build with security best practices |
| K8s Manifests | Base + staging/production overlays |
| Security | Trivy config, .trivyignore, Gitleaks config |
| Testing | Test framework, coverage config, budgets |
| Monitoring | Prometheus metrics, Grafana dashboard template |
| Documentation | README template, ADR structure, runbook template |
Platform Maturity Levels
| Level | Capability | Self-Service? |
|---|---|---|
| 1 | Template repos (copy and modify) | Partially |
| 2 | Scaffolding CLI (generate project) | Yes |
| 3 | Backstage catalog (discover + create) | Yes |
| 4 | Internal developer portal (full lifecycle) | Yes |
The Implementation
GitHub Template Repository
acme/service-template-go/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Full pipeline (CH1-CH6)
│ │ ├── security.yml # Trivy + CodeQL (CH16)
│ │ ├── performance.yml # Locust smoke test (CH17)
│ │ └── hotfix.yml # Emergency pipeline (CH21)
│ ├── CODEOWNERS
│ └── PULL_REQUEST_TEMPLATE.md
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ └── handler/
│ └── health.go
├── k8s/
│ ├── base/
│ │ ├── kustomization.yaml
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── hpa.yaml
│ └── overlays/
│ ├── staging/
│ └── production/
├── tests/
│ ├── performance/
│ │ ├── locustfile.py
│ │ └── budgets.yaml
│ └── integration/
├── Dockerfile
├── .trivyignore
├── .gitleaks.toml
├── .security-baseline.json
├── go.mod
└── README.md
Scaffolding Script
#!/bin/bash
# scripts/create-service.sh
# HARDENED: Scaffold a new service from template
set -euo pipefail
SERVICE_NAME=$1
LANGUAGE=${2:-go}
TEAM=${3:-platform}
if [[ -z "$SERVICE_NAME" ]]; then
echo "Usage: $0 <service-name> [language] [team]"
echo "Languages: go, node, java"
exit 1
fi
TEMPLATE="acme/service-template-${LANGUAGE}"
echo "Creating $SERVICE_NAME from $TEMPLATE..."
# Create repo from template
gh repo create "acme/${SERVICE_NAME}" \
--template "$TEMPLATE" \
--public \
--clone
cd "$SERVICE_NAME"
# Replace template placeholders
find . -type f -name '*.go' -o -name '*.yaml' -o -name '*.yml' \
-o -name '*.json' -o -name 'Dockerfile' -o -name '*.md' | \
xargs sed -i "s/service-template-${LANGUAGE}/${SERVICE_NAME}/g"
# Update go.mod
if [[ "$LANGUAGE" == "go" ]]; then
sed -i "s|module github.com/acme/service-template-go|module github.com/acme/${SERVICE_NAME}|" go.mod
fi
# Set up branch protection
gh api repos/acme/${SERVICE_NAME}/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["build","security","trivy"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1}'
# Create infra repo entry
cd ../ecommerce-infra
mkdir -p "apps/${SERVICE_NAME}/base" "apps/${SERVICE_NAME}/overlays/staging" "apps/${SERVICE_NAME}/overlays/production"
cp "apps/_template/base/"* "apps/${SERVICE_NAME}/base/"
sed -i "s/TEMPLATE_SERVICE/${SERVICE_NAME}/g" "apps/${SERVICE_NAME}/base/"*
git add .
git commit -m "infra: add ${SERVICE_NAME}"
git push
echo "Service $SERVICE_NAME created."
echo " Repo: https://github.com/acme/${SERVICE_NAME}"
echo " Infra: apps/${SERVICE_NAME}/"
Backstage Software Catalog
# catalog-info.yaml (in each service repo)
# HARDENED: Backstage entity descriptor
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: checkout-service
description: Handles cart checkout and order creation
annotations:
github.com/project-slug: acme/checkout-service
backstage.io/techdocs-ref: dir:.
argocd/app-name: checkout-production
prometheus.io/rule: 'sum(rate(http_requests_total{app="checkout-service"}[5m]))'
tags:
- go
- grpc
links:
- url: https://grafana.acme.com/d/checkout
title: Grafana Dashboard
- url: https://argocd.acme.com/applications/checkout-production
title: ArgoCD
spec:
type: service
lifecycle: production
owner: checkout-team
system: ecommerce
providesApis:
- checkout-api
consumesApis:
- inventory-api
- payments-api
dependsOn:
- component:inventory-service
- resource:postgres-checkout
Backstage Template for New Services
# backstage-templates/go-service.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: go-service
title: Go Microservice
description: Create a new Go microservice with full CI/CD pipeline
spec:
owner: platform-team
type: service
parameters:
- title: Service Details
required: [name, owner]
properties:
name:
title: Service Name
type: string
pattern: "^[a-z][a-z0-9-]*$"
owner:
title: Owner Team
type: string
ui:field: OwnerPicker
description:
title: Description
type: string
steps:
- id: create-repo
name: Create Repository
action: publish:github
input:
repoUrl: github.com?owner=acme&repo=${{ parameters.name }}
templateId: acme/service-template-go
repoVisibility: public
- id: create-infra
name: Create Infrastructure Entry
action: github:actions:dispatch
input:
repoUrl: github.com?owner=acme&repo=ecommerce-infra
workflow_id: add-service.yml
branch: main
parameters:
service_name: ${{ parameters.name }}
- id: register
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['create-repo'].output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Repository
url: ${{ steps['create-repo'].output.remoteUrl }}
- title: Open in Backstage
icon: catalog
entityRef: ${{ steps['register'].output.entityRef }}
The Gate
The golden path is the ultimate gate. It ensures every new service starts with the correct pipeline configuration. The platform team maintains the template. When security policy changes (new scanner, tighter threshold), they update the template once and all future services inherit it.
For existing services, create a “template sync” workflow that detects when the template has been updated and opens a PR on each service repo with the changes.
The Recovery
Template diverges from actual service needs: Not every service fits the template perfectly. Allow customization by overlay: the template provides the base, services can add their own workflows. But they cannot remove the security and testing gates.
Backstage catalog is stale: Automate catalog registration. When a new repo is created from the template, the scaffolding script registers it in Backstage. When a repo is archived, the catalog entry is removed.
Golden path becomes golden cage: The template should be opinionated about gates (security, testing, deployment) but flexible about implementation (language, framework, database). Platform teams that dictate too many implementation choices lose developer trust.