Published on

Platform Engineering — Building an Internal Developer Platform That Engineers Actually Use

Authors

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)

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:

  1. Engineer discovers service template in Backstage
  2. Engineer generates new service from template (auto-creates Git repo, CI/CD, monitoring)
  3. Engineer deploys via git push (no manual infra work)
  4. 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.