Skip to main content
Platform Engineering

Templates and Scaffolding

Ravinder··5 min read
Platform EngineeringDevOpsIDPBackstageScaffolding
Share:
Templates and Scaffolding

New service creation is a surprisingly good proxy for platform maturity. At an immature org it takes a sprint: find the right IAM permissions, clone someone's existing repo and gut it, figure out which CI template to use, manually register the service somewhere. At a mature one it takes twenty minutes and a form.

Scaffolding is how you close that gap. Templates encode the golden path decisions from the previous post into runnable starting points. A new engineer should be able to create a production-ready service skeleton without talking to anyone on the platform team.

The Scaffolding Stack

Three tools dominate this space. They're not mutually exclusive.

Backstage Software Templates. The full IDP approach. You get a UI, a catalog, plugin ecosystem, and a workflow engine. High ceiling, high setup cost. Best when you need this to be a visible, self-service product that non-technical stakeholders can see.

cookiecutter / copier. CLI-first. Lower setup cost, easier to version-control, works without a running service. Best for smaller teams or as the underlying engine that Backstage calls.

Bespoke scaffolders. A script, a Go binary, a GitHub Action — whatever fits your stack. Sometimes the right answer is a 200-line Python script that does exactly what you need and nothing else.

flowchart TD A[Engineer wants new service] --> B{Which tool?} B -- UI preferred / large org --> C[Backstage Software Template] B -- CLI preferred / small org --> D[copier / cookiecutter] B -- Very specific needs --> E[Bespoke script] C --> F[Template renders] D --> F E --> F F --> G[Repo created] G --> H[CI configured] H --> I[Service registered in catalog] I --> J[First deploy pipeline ready]

What to Bake In

The scaffolding decision tree: if every new service of type X needs it, bake it in. If only some services need it, make it an optional flag. If one service needs it, let that team handle it.

Always bake in:

  • Directory structure and language-appropriate linter config
  • Dockerfile following your golden path base image
  • Reusable CI workflow reference (not a copy — a uses: reference)
  • catalog-info.yaml stub pre-filled with service name and owner
  • .trivyignore or equivalent pointing to your CVE policy
  • Standard health check endpoint
  • OWNERS or CODEOWNERS file

Offer as options:

  • Database migration setup (flag: --database postgres)
  • gRPC vs HTTP API scaffolding
  • Queue consumer boilerplate
  • Async worker vs synchronous API

Leave out entirely:

  • Business logic (obvious, but scaffolders get feature-crept)
  • Tool installs that belong in dev container config
  • Anything that will be wrong in six months

A Minimal copier Template

# Directory layout
templates/
  python-api/
    copier.yaml
    {{project_name}}/
      __init__.py
      main.py
      health.py
    Dockerfile
    .github/
      workflows/
        ci.yml
    catalog-info.yaml
# copier.yaml
questions:
  project_name:
    type: str
    help: "Service name (lowercase, hyphens ok)"
  team_name:
    type: str
    help: "Owning team slug"
  needs_database:
    type: bool
    default: false
    help: "Does this service need a Postgres database?"
 
tasks:
  - "pip install -r requirements.txt"
  - "git init"
  - "git add ."
  - "git commit -m 'chore: scaffold from platform template'"
# {{project_name}}/health.py — always included
from fastapi import APIRouter
 
router = APIRouter()
 
@router.get("/health/live")
def liveness():
    return {"status": "ok"}
 
@router.get("/health/ready")
def readiness():
    # Override this in your service to check real dependencies
    return {"status": "ok"}

Backstage Software Template Example

For teams that have Backstage, a Software Template is a YAML file in your catalog:

# templates/python-api/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: python-api
  title: Python API Service
  description: FastAPI service with observability, CI, and catalog registration baked in
  tags:
    - python
    - api
    - recommended
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: Owning team
          type: string
          ui:field: OwnerPicker
        needsDatabase:
          title: Needs Postgres database?
          type: boolean
          default: false
 
  steps:
    - id: fetch-template
      name: Fetch template files
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          owner: ${{ parameters.owner }}
          needsDatabase: ${{ parameters.needsDatabase }}
 
    - id: create-repo
      name: Create GitHub repo
      action: publish:github
      input:
        repoUrl: github.com?owner=my-org&repo=${{ parameters.name }}
        defaultBranch: main
 
    - id: register-catalog
      name: Register in catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['create-repo'].output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

Keeping Templates Fresh

Templates rot. The language version you baked in six months ago has a CVE. The CI workflow reference points to a deprecated action. The base Docker image is three major versions behind.

Treat templates as living code:

  • Pin to specific versions, not latest
  • Run a weekly job that opens a PR against the template itself when dependencies need updating
  • When you update a template, provide a migration guide (or a migration script) for services already scaffolded from it
# .github/workflows/template-audit.yml
name: Template dependency audit
on:
  schedule:
    - cron: '0 9 * * 1'  # Mondays
 
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check base image freshness
        run: |
          python scripts/check_template_deps.py \
            --template templates/python-api \
            --fail-on-major-drift

Key Takeaways

  • Scaffolding turns golden path decisions into executable starting points — the goal is production-readiness in minutes, not days.
  • Backstage Software Templates suit larger orgs that need a visible, UI-driven self-service workflow; copier/cookiecutter suits smaller teams or serves as the engine underneath Backstage.
  • Bake in what every service of a type needs: CI reference, health endpoint, catalog stub, linter config. Use flags for common optional features. Leave business logic out entirely.
  • Templates are living code — pin dependency versions and run automated audits to catch drift before engineers scaffold from a stale template.
  • A migration path for already-scaffolded services is as important as the template itself; without it, every template update creates N divergent forks.
  • The best scaffolding disappears: engineers stop thinking about it because it just works.
Share: