Platform Engineering
Best practices for building Internal Developer Platforms (IDPs) that reduce cognitive load, accelerate delivery, and create golden paths for development teams.
IDP Architecture Layers
A well-designed IDP separates concerns into distinct layers. Each layer abstracts complexity from the one above it.
Developer Interface (Portal / CLI / API)
|
Orchestration Layer (Workflows, Templates, Scaffolding)
|
Integration Layer (APIs, Plugins, Connectors)
|
Resource Layer (Infrastructure, Services, Tools)
| Layer | Purpose | Components | Owned By | |-------|---------|------------|----------| | Developer Interface | Self-service entry point | Backstage portal, CLI tools, API gateway | Platform team | | Orchestration | Workflow automation, templating | Scaffolder, Terraform modules, Crossplane | Platform team | | Integration | Connect tools and services | Backstage plugins, API adapters, webhooks | Platform + tool owners | | Resource | Actual infrastructure and services | Kubernetes, databases, CI/CD, monitoring | Infrastructure team | | Governance | Policy enforcement and compliance | OPA, Kyverno, cost policies, security scans | Security + platform team |
Platform Team Topology and Responsibilities
Team Structure
| Role | Responsibility | Focus Area | |------|---------------|------------| | Platform Product Manager | Roadmap, prioritization, user research | Developer needs, adoption metrics | | Platform Engineer | IDP core, golden paths, automation | Infrastructure abstraction, tooling | | Developer Advocate | Documentation, onboarding, feedback loops | DevEx, training, communication | | SRE/Reliability Lead | Platform reliability, SLOs, incident response | Uptime, performance, observability | | Security Engineer | Policy-as-code, compliance automation | Guardrails, scanning, access control |
Interaction Model
Stream-Aligned Teams (consumers)
|
| self-service requests
v
Platform Team (enablers)
|
| golden paths, templates, APIs
v
Infrastructure / Cloud (resources)
Platform teams operate as enabling teams (Team Topologies model). They reduce cognitive load on stream-aligned teams by providing curated, opinionated abstractions.
Backstage: Service Catalog and Developer Portal
catalog-info.yaml -- Service Registration
Every service registers itself in the catalog via a catalog-info.yaml at the repo root.
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-service
description: Handles payment processing and refunds
annotations:
github.com/project-slug: acme-corp/payment-service
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: P1234ABC
grafana/dashboard-selector: "payment-service"
tags:
- java
- spring-boot
- payments
links:
- url: https://grafana.internal/d/payments
title: Dashboard
icon: dashboard
- url: https://runbooks.internal/payments
title: Runbook
icon: docs
spec:
type: service
lifecycle: production
owner: team-payments
system: checkout-system
providesApis:
- payment-api
consumesApis:
- inventory-api
- notification-api
dependsOn:
- resource:payments-db
- component:auth-service
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: payment-api
description: Payment processing REST API
spec:
type: openapi
lifecycle: production
owner: team-payments
system: checkout-system
definition:
$text: ./openapi.yaml
Service Catalog API -- Querying Components
# List all components owned by a team
curl -s "https://backstage.internal/api/catalog/entities?filter=kind=component,spec.owner=team-payments" \
-H "Authorization: Bearer $BACKSTAGE_TOKEN" | jq '.[] | {name: .metadata.name, lifecycle: .spec.lifecycle}'
# Find all services consuming a specific API
curl -s "https://backstage.internal/api/catalog/entities?filter=kind=component,spec.consumesApis=payment-api" \
-H "Authorization: Bearer $BACKSTAGE_TOKEN" | jq '.[] | .metadata.name'
# Get component details with relations
curl -s "https://backstage.internal/api/catalog/entities/by-name/component/default/payment-service" \
-H "Authorization: Bearer $BACKSTAGE_TOKEN" | jq '{
name: .metadata.name,
owner: .spec.owner,
apis: .spec.providesApis,
dependencies: .spec.dependsOn
}'
Golden Path Templates
Golden paths are opinionated, pre-configured templates that encode best practices. They give teams a paved road to production.
Backstage Software Template
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: spring-boot-service
title: Spring Boot Microservice
description: Creates a production-ready Spring Boot service with CI/CD, monitoring, and database
tags:
- java
- spring-boot
- recommended
spec:
owner: platform-team
type: service
parameters:
- title: Service Details
required:
- name
- owner
- description
properties:
name:
title: Service Name
type: string
pattern: '^[a-z][a-z0-9-]*$'
ui:autofocus: true
owner:
title: Owner Team
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
description:
title: Description
type: string
javaVersion:
title: Java Version
type: string
default: '21'
enum: ['17', '21']
- title: Infrastructure
properties:
database:
title: Database
type: string
default: postgresql
enum: [postgresql, mysql, none]
cacheLayer:
title: Cache Layer
type: string
default: none
enum: [redis, none]
messageBroker:
title: Message Broker
type: string
default: none
enum: [kafka, rabbitmq, none]
steps:
- id: fetch-template
name: Fetch Skeleton
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
description: ${{ parameters.description }}
javaVersion: ${{ parameters.javaVersion }}
database: ${{ parameters.database }}
- id: create-repo
name: Create Repository
action: publish:github
input:
repoUrl: github.com?owner=acme-corp&repo=${{ parameters.name }}
description: ${{ parameters.description }}
defaultBranch: main
protectDefaultBranch: true
requireCodeOwnerReviews: true
- id: register-catalog
name: Register in Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps['create-repo'].output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
- id: create-argocd-app
name: Create ArgoCD Application
action: argocd:create-resources
input:
appName: ${{ parameters.name }}
repoUrl: ${{ steps['create-repo'].output.remoteUrl }}
output:
links:
- title: Repository
url: ${{ steps['create-repo'].output.remoteUrl }}
- title: Service in Catalog
url: ${{ steps['register-catalog'].output.entityRef }}
- title: CI/CD Pipeline
url: ${{ steps['create-repo'].output.remoteUrl }}/actions
Golden Path Coverage Matrix
| Category | What the Golden Path Provides | Without Golden Path | |----------|------------------------------|---------------------| | Repository | Pre-configured with CI/CD, linting, CODEOWNERS | Manual setup, inconsistent configs | | CI/CD | Working pipeline from day one | Copy-paste from other repos, broken configs | | Observability | Dashboards, alerts, SLOs pre-configured | No monitoring until first incident | | Security | Dependency scanning, SAS