Platform engineering is the practice of building an internal product, usually called an internal developer platform, that lets application teams create, deploy and run software through a small set of self-service, pre-approved paths. Instead of every team learning Kubernetes manifests, Terraform modules, CI syntax, secret stores and dashboards from scratch, a platform team packages those into golden paths: opinionated, supported ways to do common jobs, with security, cost and compliance built in.

The idea is easy to state and easy to get wrong. Many platform efforts end up as a portal nobody opens, or a ticket queue with a new name. This article explains the architecture from first principles, the five planes a platform is made of and how a request flows through them, then walks a worked example, "a new service in production in under an hour", with the real configuration that makes it work. It finishes with how to measure a platform, the failure modes that sink them, and a concrete checklist.

Internal developer platform: five planes, one paved roadDeveloper interfacesportal, CLI, API, pull requests: same actions, different front doorsOrchestration planetemplates, workflows, scaffolderSoftware catalogowners, systems, APIs, scorecardsregistersPolicy planepolicy as code, admission, cost capsControl planeGit as desired state, IaC, reconcilersallowedClustersruntime, namespacesCloud resourcesdatabases, queues, bucketsShared servicesCI, secrets, observabilityPlatform team as product teamroadmap from user research, adoption and lead-time metrics, SLOs for the platform itselffeedback
Interfaces, orchestration and the catalog sit on a policy plane and a Git-driven control plane; the platform team runs the whole thing as a product.

Why platforms exist: cognitive load

The underlying problem is cognitive load. A stream-aligned team, in the Team Topologies vocabulary, should spend its attention on its domain. Every piece of infrastructure it must understand in detail, how to write a Helm chart, which IAM policy a bucket needs, how to wire alerts, takes attention away from that. Multiply by forty teams and you get forty slightly different CI pipelines, forty ways to run a database migration, and a security team that cannot answer which services are internet-facing.

A platform turns repeated decisions into defaults. The important word is self-service: if a developer has to file a ticket and wait for a human, you have built an operations team, not a platform. The second important word is optional: a good golden path is the easiest way to do the job, not the only way, and teams with unusual needs can step off it and take on the responsibility that comes with that. Paths that must be forced on people are usually paths that are not good enough yet.

Why platforms exist: cognitive load

The underlying problem is cognitive load. A stream-aligned team, in the Team Topologies vocabulary, should spend its attention on its domain. Every piece of infrastructure it must understand in detail, how to write a Helm chart, which IAM policy a bucket needs, how to wire alerts, takes attention away from that. Multiply by forty teams and you get forty slightly different CI pipelines, forty ways to run a database migration, and a security team that cannot answer which services are internet-facing.

A platform turns repeated decisions into defaults. The important word is self-service: if a developer has to file a ticket and wait for a human, you have built an operations team, not a platform. The second important word is optional: a good golden path is the easiest way to do the job, not the only way, and teams with unusual needs can step off it and take on the responsibility that comes with that. Paths that must be forced on people are usually paths that are not good enough yet.

The five planes of an internal developer platform

Real platforms differ in tools but share the same planes, shown in the diagram above.

  • Developer interfaces. A portal (Backstage is the common open-source base; Port and Cortex are hosted alternatives), a CLI, an API and plain pull requests. Each is a front door to the same actions. Many developers prefer the CLI and Git; the portal earns its place through discovery and the catalog, not forms.
  • Orchestration plane. Templates and workflows that turn intent ("new Go service owned by team payments") into concrete changes: a repository from a skeleton, CI configuration, a catalog entry, an environment request.
  • Software catalog. The system of record for what exists and who owns it: components, APIs, resources, systems, teams. Everything else reads from it, from on-call routing to cost reports. See the service catalog guide for modelling advice.
  • Policy plane. Rules expressed as code and evaluated automatically: in CI on every pull request, and again at admission when resources reach the cluster or cloud. Policy is how a platform says yes quickly instead of no slowly.
  • Control plane. Git holds desired state; reconcilers (GitOps controllers for Kubernetes, infrastructure-as-code pipelines or Kubernetes-native resource controllers for cloud resources) make reality match it continuously. This is what makes actions auditable and reversible: a revert is a rollback.
  • Resource plane. The clusters, cloud accounts, CI runners, secret stores and observability stack the platform team operates and the abstractions sit on.

The key design choice is the level of abstraction. Too low, and developers still need to understand every underlying tool. Too high, and the first unusual requirement forces a team off the platform entirely. A useful test: the abstraction should expose decisions the developer is qualified to make (size, tier, data classification) and hide the ones they are not (subnet layout, encryption configuration).

Golden paths: templates that wire everything up

A golden path is a template plus everything it wires up. In Backstage the scaffolder reads a Template entity: parameters become a form (or CLI prompts), and steps call actions. The actions below are real built-ins: fetch a skeleton and render it, publish a repository, register the result in the catalog.

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: go-http-service
  title: Go HTTP service
  description: Service with CI, deploy manifests, SLO dashboard and runbook
spec:
  owner: group:platform
  type: service
  parameters:
    - title: Service details
      required: [name, owner, tier]
      properties:
        name:
          type: string
          pattern: '^[a-z][a-z0-9-]{2,30}$'
        owner:
          type: string
          ui:field: OwnerPicker
        tier:
          type: string
          enum: [tier-1, tier-2, tier-3]
  steps:
    - id: fetch
      name: Render skeleton
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          owner: ${{ parameters.owner }}
          tier: ${{ parameters.tier }}
    - id: publish
      name: Create repository
      action: publish:github
      input:
        repoUrl: github.com?owner=acme&repo=${{ parameters.name }}
    - id: register
      name: Register in catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

The skeleton carries the real value: a catalog-info.yaml with owner and lifecycle, a CI pipeline that calls shared, versioned workflow definitions rather than copying them, deploy manifests that reference a platform-maintained base, pre-built SLO alerts, and a runbook stub. Because CI and deploy logic are referenced, not copied, a fix in the shared definition reaches every service; copied templates drift from the day they are generated.

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: invoice-api
  annotations:
    github.com/project-slug: acme/invoice-api
spec:
  type: service
  lifecycle: production
  owner: group:payments
  system: billing

Guardrails as policy as code

Guardrails belong in code that runs on every change, not in a review meeting. The same rule should run in CI, so developers learn about a violation in their pull request, and at admission, so nothing bypasses CI. Here is a Conftest policy in current Rego syntax that rejects Kubernetes Deployments without an owner label or memory limits:

package main

deny contains msg if {
  input.kind == "Deployment"
  not input.metadata.labels.owner
  msg := sprintf("%s: missing metadata.labels.owner", [input.metadata.name])
}

deny contains msg if {
  input.kind == "Deployment"
  some c in input.spec.template.spec.containers
  not c.resources.limits.memory
  msg := sprintf("%s/%s: set resources.limits.memory", [input.metadata.name, c.name])
}

Write every rule with a message that says how to fix it, and ship new rules in warn mode first: count violations across the fleet, fix the templates, then switch to deny. A rule that suddenly blocks two hundred deploys teaches teams that the platform is an obstacle. Cost guardrails follow the same pattern: tag resources with owner and environment at creation, enforce the tags by policy, and alert teams on budget, rather than approving spend by hand.

Worked example: a new service in under an hour

Team payments needs a new invoice API. On the old path this took eleven tickets and about three weeks. On the platform path:

  1. Minute 0: the developer runs the Go service template, enters name, owner and tier. The scaffolder creates the repository and registers it in the catalog.
  2. Minute 5: the first CI run builds, tests, scans the image and runs policy checks against the rendered manifests. All pass because the skeleton was built to pass.
  3. Minute 12: the developer opens a pull request that adds a Postgres database by declaring a small claim (size, tier, data classification) in the service repository. Policy checks that the tier matches the service tier and that confidential data gets encryption and backups.
  4. Minute 25: after merge, the control plane provisions the database, writes connection credentials into the secret store and the deploy picks them up. Nobody opened a cloud console.
  5. Minute 40: the service is live in staging with traces, metrics, logs, an SLO dashboard and a paging route to the payments on-call, all derived from the catalog entry.
  6. Minute 55: promotion to production is a pull request against the environment repository, approved by the team itself, because the policy plane already enforced what a reviewer would have checked.

Measure that flow end to end. The platform's headline metric is time from "I need a service" to "first production request", and it should be tracked as carefully as any product funnel.

Notice what the developer did not do: write a pipeline, choose an instance type, request firewall rules, or configure dashboards by hand. Every one of those decisions still happened, but it was made once by the platform team, encoded in the template and the policies, reviewed like code, and inherited by every service created afterwards. When the platform team later improves the database backup policy, every claim picks up the change on its next reconcile.

Measuring a platform

Run the platform as a product: it has users, a roadmap, user research and success metrics. Useful measures fall into three groups.

GroupMetricWhy it matters
DeliveryDeployment frequency, lead time for changesDORA throughput metrics; should improve for adopters
StabilityChange failure rate, time to restorePaved roads should not trade speed for incidents
AdoptionShare of services on golden paths, template version spreadLow adoption means the path is not better yet
ExperienceOnboarding time, survey scores, tickets per teamCognitive load made visible
Platform healthPlatform SLOs: scaffolder, CI and deploy success rate and latencyA broken platform blocks everyone

Avoid vanity measures such as portal page views. The developer experience guide covers survey design, and the SLO program guide applies equally to the platform's own services: the deploy pipeline needs an SLO as much as checkout does.

Failure modes

  • Portal first, platform never. A beautiful portal fronting ticket queues. Build self-service actions first; the portal is a thin layer over them.
  • The mandated path. Adoption forced by decree hides the fact that the path is worse than what teams had. Win adoption by being better, and treat escapes as user research.
  • Template drift. Copied pipelines and manifests freeze on the day of generation. Reference shared, versioned building blocks and track which version each service runs.
  • Leaky or rigid abstraction. Either developers still need the underlying tool's manual, or the first special case requires leaving the platform. Provide escape hatches with clear ownership boundaries.
  • Stale catalog. Ownership data entered by hand rots, and on-call routing pages the wrong team. Keep catalog files in the service repositories, validate them in CI, and flag orphaned components.
  • Platform as single point of failure. When the CI or deploy system is down nobody ships, including the fix. Give the platform SLOs, on-call and a documented break-glass path.

Trade-offs

DecisionOption AOption BGuidance
PortalSelf-hosted open sourceHosted productSelf-host if you have front-end capacity to maintain plugins
Cloud resourcesIaC modules via pipelinesKubernetes-native resource controllersPick one per resource type and hide it behind a claim
PolicyCI onlyCI plus admissionBoth: CI for feedback, admission for enforcement
ScopeThinnest viable platformFull internal cloudStart thin: one golden path done well

Infrastructure-as-code fundamentals, which most of the control plane rests on, are in the Terraform introduction.

What to do next

  1. Interview five teams and list the three most repeated, most painful infrastructure jobs.
  2. Pick one, usually "create a service", and build a golden path that does it end to end, self-service.
  3. Stand up a catalog with owner, lifecycle and system for every service, stored next to the code.
  4. Express the first five guardrails as policy as code; run them in warn mode, then deny.
  5. Reference shared CI and deploy definitions instead of copying them, and track versions per service.
  6. Measure time to first production request plus the DORA metrics for adopting teams.
  7. Give the platform its own SLOs, on-call rotation and break-glass procedure.
Key takeaway: An internal developer platform is a product that turns repeated infrastructure decisions into self-service golden paths. Build it in planes, with interfaces, orchestration, a catalog, policy as code and a Git-driven control plane, start with one path done well, reference shared building blocks instead of copying them, and measure adoption and lead time rather than portal traffic.