Golden Paths, How Self-Service Actually Sticks
TL;DR — A golden path is a template plus a contract. The template is the easy part. The contract — opinionated defaults, owned upgrades, a clear off-ramp — is what keeps a team on the path past day 30.
In last week’s IDP build-out I waved at “ship one golden path.” That undersells how hard the path part is. The scaffolder template that generates a repo is maybe 20 percent of the work. The rest is what happens after the first git commit lands.
I’ve seen platform teams ship six templates and watch every consumer fork the output within two months. That isn’t a self-service platform. That’s a copy-paste service with extra YAML. This post is about the difference.
The path is not the template
Spotify popularized “golden path” with a deliberate metaphor — paved road through the forest. The path is the route, not the brochure handed out at the trailhead. A scaffolder template generates the brochure. The path keeps existing tomorrow.
A useful definition: a golden path is the supported way to build, ship, and operate a service of a given shape, including how it gets upgraded over time. Three components:
- An opinionated starting point — the template.
- Operational defaults that the platform owns — base images, CI workflows, observability wiring, IAM patterns.
- A continuous integration of upstream changes — when the platform team updates the path, existing services on the path pick up the change.
Most teams nail #1, half-nail #2, and skip #3 entirely. Skipping #3 is why services drift off the path within a quarter.
What “opinionated” means
The fastest way to lose adoption is to ship a template with twelve optional inputs. Every choice you push onto the developer is a choice you have to support forever and a moment where they question whether the path is worth it.
A path I shipped at a previous job started with this template input list:
- Service name
- Owning team
- Initial replicas
- Pod requests / limits
- Database engine (Postgres or MySQL)
- Cache (Redis or Memcached)
- Auth provider
- Initial canary percentage
- Helm chart version
Adoption was awful. We ran user interviews and the consistent feedback was: “I don’t know what to pick. I just want a service.” We collapsed the inputs to three — name, team, system — and adoption tripled in eight weeks.
# Before — every choice on the user
parameters:
- properties:
name: { type: string }
replicas: { type: integer }
db_engine: { type: string, enum: [postgres, mysql] }
cache_engine: { type: string, enum: [redis, memcached] }
pod_cpu: { type: string }
pod_memory: { type: string }
# After — opinionated defaults baked in
parameters:
- properties:
name: { type: string }
owner: { type: string, ui:field: OwnerPicker }
system: { type: string }
Postgres is the default because we already operate Postgres. Replicas default to 2. CPU and memory come from a sensible profile based on service type. If someone needs to deviate, they can, after we talk to them. The act of needing to talk to us is the friction we want.
The platform-owned bits
Look at the contents of a service repo. Some files belong to the application team. Some belong to the platform team. The mistake is treating them all as equal.
Application-owned, application-edited:
cmd/,internal/, business logic- The application-level tests
- The
README.md - The
catalog-info.yaml(the team owns its identity)
Platform-owned, application-consumed:
- The base
Dockerfile - The
.github/workflows/CI files - The Kubernetes manifest skeleton
- The OpenTelemetry initialization snippet
- The auth middleware import
- The default linter and formatter config
Application teams should never be editing the CI workflow. If they need to, that’s a platform bug. The way I enforce this in practice is by shipping these files via a reusable workflow or a base image that the template just references:
# .github/workflows/ci.yml in the generated service
name: CI
on: [push, pull_request]
jobs:
build:
uses: acme/platform-workflows/.github/workflows/go-service-ci.yml@v3
with:
service_name: orders-api
secrets: inherit
That @v3 pin is doing real work. The platform team controls the actual workflow definition in a separate repo. When v4 ships, the template generates services pinned to v4 by default and existing services can bump on their own time.
Versioning the path
This is the piece almost everyone skips. A golden path has versions and a deprecation policy or it’s not a path — it’s a one-time gift.
Approaches I’ve seen work:
- Reusable workflows pinned to tags. As above. Cheap, works for CI and partial automation.
- Base images with semver tags.
acme/go-runtime:1-bookwormfollows minor versions automatically;acme/go-runtime:1.21.5-bookwormpins precisely. - Renovate or Dependabot for the path. A platform-team-owned bot opens PRs on every service when a path version bumps. The application team merges on their schedule.
// renovate.json shipped with every templated service
{
"extends": ["github>acme/platform-renovate-presets:go-service"],
"schedule": ["after 2am every weekday"]
}
That platform-renovate-presets repo is platform-owned. The application team imports a preset they don’t have to think about. When the platform ships a new approved version of the base image, Renovate opens a PR on every service in the org.
The off-ramp matters
You cannot ship a golden path with no exit. Some service will eventually need to deviate — a team needs a different database engine, an ML workload doesn’t fit the standard pod shape, a legacy migration needs custom CI. The minute the path becomes a prison, adoption craters.
A useful three-tier model:
- On the path — fully templated, platform-supported, automatic upgrades.
- Off the path with help — used the template but customized parts; platform team consults but doesn’t own.
- Off the path — fully custom; documented as an exception with a return plan.
A team going to tier 2 should know exactly what they’re giving up. A team going to tier 3 should have a postmortem-style doc explaining why and what would need to change for them to come back. Tier 3 is rare, by design.
Measuring path stickiness
Templates produce a repo. Stickiness is different. The metrics that actually matter:
- Percent of services still on the latest major path version (target: >70% within 30 days of release)
- Mean drift — number of platform-owned files that have been modified locally in service repos (target: low and decreasing)
- Adoption rate of the latest template version for new services (target: 95%+)
- Time to first deploy for a new service via the template
You can pull most of this out of Backstage TechInsights or a simple cron job that scrapes repos. The exact numbers matter less than the trend.
Common Pitfalls
- The “configurable everything” template. Every flag is a future support ticket and a future maintenance branch. Default it. Hide it behind an advanced section if you must.
- Forking the workflow in the template instead of referencing it. Templates that embed the entire CI workflow inline produce frozen-in-time services. Reference reusable workflows.
- No deprecation policy. v1 of the path will be embarrassing in 18 months. Decide in advance how long you support it.
- Manual upgrades on the platform team. If shipping a new path version requires the platform team to manually PR every consumer, you won’t ship updates. Automate via Renovate / Dependabot.
- Path documentation as a separate wiki. Docs live in the path repo. They version with the path. Anything else gets stale.
The one I personally got wrong: I shipped a path with too many optional inputs because I wanted to look flexible. It read as flexible to me. It read as paralysis to the developers using it. Opinions sell.
Wrapping Up
A golden path that sticks is a template plus a maintenance contract plus a clear off-ramp. Ship one. Measure adoption and drift weekly. Ship v2 only when v1 is in real use. Anything else is producing folder structures, not infrastructure.
Next post in the series pulls back from Backstage and looks at the layer underneath — the eternal Crossplane vs Terraform debate, and which one fits where in 2024. Backstage is the door. We need to talk about what’s behind it.
For deeper reading, the Backstage Software Templates documentation covers the scaffolder action API in detail.