Skip to main content
ship it and sleep

Self-Service Developer Platforms: Golden Paths and Backstage

6 min read Chapter 64 of 66

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.

Developer platform architecture

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

ComponentTemplate Contents
CI Pipeline.github/workflows/ci.yml with all gates
DockerfileMulti-stage build with security best practices
K8s ManifestsBase + staging/production overlays
SecurityTrivy config, .trivyignore, Gitleaks config
TestingTest framework, coverage config, budgets
MonitoringPrometheus metrics, Grafana dashboard template
DocumentationREADME template, ADR structure, runbook template

Platform Maturity Levels

LevelCapabilitySelf-Service?
1Template repos (copy and modify)Partially
2Scaffolding CLI (generate project)Yes
3Backstage catalog (discover + create)Yes
4Internal 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.