- Published on
Platform Engineering — Building an Internal Developer Platform That Engineers Actually Use
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
An Internal Developer Platform (IDP) enables self-service infrastructure. Bad IDPs force engineers into rigid processes. Good IDPs make the right path the easy path. This post covers IDP philosophy, Backstage setup, golden paths, and adoption metrics.
- What an IDP Is (Not Just Kubernetes)
- Golden Paths vs Golden Cages
- Backstage as Developer Portal
- Self-Service Infrastructure Templates
- Paved Road for New Services
- 3. Run end-to-end tests
- 4. Deploy to production
- Monitoring is automatic
- Off-ramp alternatives
- Common questions
- Measuring Platform Adoption and Developer Satisfaction
- IDP Anti-Patterns
- Checklist
- Conclusion
What an IDP Is (Not Just Kubernetes)
An IDP is not:
- A Kubernetes cluster (that's infrastructure)
- A CI/CD pipeline (that's part of it)
- A container registry (also part of it)
An IDP is a product for engineers, providing:
- Service catalog: discoverable services and their owners
- Scaffolding templates: create new services in minutes
- Paved roads: documented, proven architectures
- Internal APIs: standardized authentication, databases, queues
- Developer portal: unified view of all services
Example IDP flow:
- Engineer discovers service template in Backstage
- Engineer generates new service from template (auto-creates Git repo, CI/CD, monitoring)
- Engineer deploys via
git push(no manual infra work) - Platform team monitors and evolves the paved road
Golden Paths vs Golden Cages
Golden path: The recommended way to do something, but other options exist. Engineers can deviate with justification.
Golden cage: The only way to do something. Forces all engineers onto one path, frustrating those with different needs.
# Good IDP: Python microservice template
# BUT engineers can also use Go, Node.js, Rust if justified
templates:
- name: python-microservice
description: Recommended template
inputs:
- name: service_name
- name: database_type
type: select
options: [postgres, mongodb, dynamodb]
- name: go-service # Alternative path
description: For performance-critical services
- name: rust-service # Another option
description: For crypto or embedded work
Backstage as Developer Portal
Backstage is an open-source developer portal. It provides:
- Software catalog (YAML registry of services)
- Scaffolder (template-based service generation)
- TechDocs (documentation site generator)
- Plugins (integrations with GitHub, Datadog, PagerDuty)
# catalog/services.yaml - Register services in Backstage
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: order-service
description: Processes customer orders
tags:
- microservice
- payment
spec:
type: service
owner: team-ecommerce
lifecycle: production
providesApis:
- order-api
consumesApis:
- payment-api
- inventory-api
dependsOn:
- resource:postgres-main
- resource:redis-cache
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: order-api
spec:
type: openapi
owner: team-ecommerce
definition:
$text: https://github.com/example/order-service/blob/main/openapi.yaml
Self-Service Infrastructure Templates
Templates auto-generate boilerplate. New service takes 5 minutes, not days.
# scaffolder-template.yaml - Create new Node.js service
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: create-nodejs-service
title: Create Node.js Microservice
description: Generate a new Node.js service with monitoring
spec:
owner: platform-team
type: service
parameters:
- title: Service Details
required: [serviceName, teamName]
properties:
serviceName:
type: string
title: Service Name
description: e.g., payment-service
teamName:
type: string
title: Team Name
enum:
- team-ecommerce
- team-payments
- team-platform
database:
type: string
title: Database Type
enum:
- postgres
- dynamodb
- none
steps:
- id: fetch
name: Fetch template
action: fetch:template
input:
url: ./template
targetPath: ./template-out
values:
serviceName: ${{ parameters.serviceName }}
teamName: ${{ parameters.teamName }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: ['github.com']
description: Generated ${{ parameters.serviceName }}
repoUrl: github.com?owner=example&repo=${{ parameters.serviceName }}
- id: register
name: Register in catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'
Paved Road for New Services
Document the recommended path. Don't hide alternatives, but make the standard path obvious.
# Paved Road: Node.js Microservice
## Fastest way to production (5 min setup, 30 min first deploy)
### 1. Generate service from template
Use Backstage template: [Create Node.js Microservice]
### 2. Deploy to staging
```bash
git push origin main # CI/CD auto-deploys to staging
3. Run end-to-end tests
npm run test:e2e
4. Deploy to production
git tag v1.0.0
git push origin --tags
Monitoring is automatic
- Logs: CloudWatch
- Metrics: Prometheus
- Traces: Jaeger
- Alerts: PagerDuty
No configuration needed. Default alerting rules applied.
Off-ramp alternatives
- Need Rust? See [Paved Road: Rust Service]
- Custom database? Requires platform review
- Serverless? Use [Lambda template]
Common questions
- How do I add a new dependency? See [Dependency Management]
- How do I run migrations? See [Database Migrations]
## Platform Team as Product Team
Treat platform as a product. Measure adoption, feedback, and satisfaction.
```typescript
// platform-metrics.ts - Track IDP health
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('platform-team');
// New services created this month
export const newServicesCreated = meter.createCounter('platform.new_services_created', {
description: 'New services created via scaffolder',
});
// Engineer satisfaction (quarterly survey)
export const engineerSatisfaction = meter.createGauge('platform.engineer_satisfaction', {
description: 'Net promoter score for platform',
});
// Time to first deploy
export const timeToFirstDeploy = meter.createHistogram('platform.time_to_first_deploy', {
description: 'Time from template generation to first production deploy',
unit: 'minutes',
});
// Template usage
export const templateUsage = meter.createCounter('platform.template_usage', {
description: 'How often each template is used',
});
Measuring Platform Adoption and Developer Satisfaction
# quarterly-survey.yaml - Measure adoption
questions:
- "How easy is it to deploy a new service? (1-10)"
- "How much time do you spend on toil? (hours/week)"
- "Would you recommend the platform to a new hire? (yes/no)"
metrics:
service_adoption: % of new services using templates
template_usage: % of teams using paved roads
time_to_deploy: hours from code commit to production
engineer_satisfaction: NPS score
targets:
service_adoption: > 90%
template_usage: > 80%
time_to_deploy: < 2 hours
engineer_satisfaction: > 50 NPS
IDP Anti-Patterns
Anti-pattern 1: Golden cage
- Platform enforces one language, one database, one cloud provider
- Engineers with different needs are blocked
- Fix: Allow off-ramp; make paved road obvious
Anti-pattern 2: Abandoned platform
- Platform team builds IDP, then leaves for other projects
- No one maintains templates or updates docs
- Fix: Assign platform team for 2+ years; make it a career
Anti-pattern 3: Over-engineering
- Platform builds every possible feature upfront
- 90% unused; complexity scares off users
- Fix: Start with catalog + one template; iterate based on feedback
Anti-pattern 4: Ineffective communication
- Great platform exists, but engineers don't know about it
- Low adoption due to visibility
- Fix: Monthly demos, office hours, Slack channel
Checklist
- Start with software catalog (register services in YAML)
- Build one paved road template first (not 20)
- Measure adoption: % of new services using templates
- Gather feedback: quarterly surveys from engineers
- Provide off-ramps: document alternatives (Rust, serverless, etc.)
- Assign dedicated platform team (2+ engineers)
- Use Backstage or similar portal
- Document paved road clearly (show happy path first)
- Monitor template satisfaction (which templates used most?)
- Iterate: monthly updates based on engineer feedback
Conclusion
Platform engineering is about removing friction for engineers. Good IDPs make the right path the easy path. Start small (catalog + one template), gather feedback, and iterate. Bad IDPs add bureaucracy; good IDPs enable autonomy. Your IDP's success is measured in engineer satisfaction and adoption, not in features built.