Platform as a Product

Treat internal platforms as products with users, roadmaps, and feedback loops. Covers platform product management, developer experience metrics, adoption strategies, golden paths, and the patterns that turn internal platforms from mandated tools into beloved products.

Platform as a Product is the discipline of treating a shared engineering platform as a first-class software product: it has a defined roadmap, a user base, a support model, a SLA, and a feedback loop. It matters when teams across an organisation rely on common services—CI/CD, deployment, monitoring, infrastructure provisioning, and developer tooling—to build and ship features faster, with higher consistency and fewer operational surprises. This is not just a technical stack; it is a product that evolves to meet the needs of its internal customers: developers, site reliability engineers, and product managers.

Onboarding a New Service to the Platform

When a new service team joins the platform, their onboarding process must be predictable, repeatable, and self-serve. The platform exposes a standard way to register a service, define its lifecycle, and integrate with shared tooling.

The process starts with git-based service registration:

git clone https://platform.example.com/registry.git
cd registry
mkdir -p services/my-new-service
echo '{
  "name": "my-new-service",
  "owner": "[email protected]",
  "description": "API for user profile management",
  "team": "users",
  "runtime": "nodejs",
  "default-branch": "main",
  "ci": "github-actions",
  "monitoring": true,
  "tracing": true,
  "logging": true
}' > services/my-new-service/manifest.json
git add services/my-new-service
git commit -m "Add my-new-service to platform registry"
git push origin main

The platform automatically detects the new service and triggers a pipeline. The first ci step runs make setup in the service repository, which must be present and executable.


setup:
	@echo "Setting up environment..."
	@npm install
	@mkdir -p build
	@echo "Setup complete"

The platform’s CI engine checks for make setup and runs it. If the Makefile is missing or the setup target fails, the platform logs the error in the event stream:

ERROR: make setup failed in my-new-service
Exit code: 1
Output:
npm: command not found

This indicates that node is not available in the CI worker’s environment. The platform resolves this by providing a ci-worker.yaml file in the service root:

# ci-worker.yaml
image: node:18-alpine
cache:
  - npm
  - .next
steps:
  - run: npm install
  - run: npm run build
  - run: npm test

The platform consumes this file and uses the specified image and cache configuration. If ci-worker.yaml is missing, the platform applies a default configuration based on the runtime field in manifest.json.

Managing Service Lifecycle

Once a service is on the platform, its lifecycle is managed through declarative configuration and automated workflows.

Deploying a New Version

To deploy a new version, the service team pushes a git tag:

git tag v1.2.3
git push origin v1.2.3

The platform detects the tag and triggers a deploy workflow. The workflow reads deploy.yaml in the service root:

# deploy.yaml
target: production
strategy: rolling
rollout:
  interval: 30s
  max-in-flight: 2
  health-check:
    path: /health
    timeout: 10s
    interval: 5s
    max-failures: 3
  pre-deploy:
    - run: kubectl apply -f k8s/manifests/
    - run: echo "Pre-deploy hooks completed"
  post-deploy:
    - run: curl -X POST https://monitoring.example.com/health
    - run: echo "Post-deploy hooks completed"

The platform executes the workflow in order. If any step fails, the rollout stops and sends a notification to the service team via email and Slack.

If health-check.interval is not set, the platform falls back to a default value of 10s. If strategy is blue-green but blue-green is not a recognized strategy, the platform logs:

WARNING: Unknown deployment strategy 'blue-green'. Using 'rolling' as fallback.

The platform also supports a rollout.yaml file for more complex scenarios. When deploy.yaml and rollout.yaml both exist, the platform merges them, with deploy.yaml taking precedence.

Promoting to Production

Promotion is handled via a promotion.yaml file:

# promotion.yaml
stages:
  - name: staging
    target: staging
    approval:
      required: true
      reviewers: [ "dev-lead", "platform-engineer" ]
    rollout:
      interval: 60s
      max-in-flight: 3
  - name: production
    target: production
    approval:
      required: true
      reviewers: [ "platform-engineer", "cto" ]
    rollout:
      interval: 15s
      max-in-flight: 1

When a service team pushes a version to staging, the platform creates a promotion request. The request includes:

The platform polls https://api.platform.example.com/v1/services/{name}/promotion-status every 30 seconds. The response is a JSON object:

{
  "service": "my-new-service",
  "current-stage": "staging",
  "next-stage": "production",
  "status": "waiting-for-approval",
  "approval-required": true,
  "reviewers": [
    {
      "user": "dev-lead",
      "status": "pending",
      "timestamp": "2024-04-05T10:00:00Z"
    },
    {
      "user": "platform-engineer",
      "status": "approved",
      "timestamp": "2024-04-05T10:05:00Z"
    }
  ],
  "deployment": {
    "id": "dpl-7a3f2b",
    "target": "staging",
    "status": "completed",
    "started": "2024-04-05T10:00:00Z",
    "finished": "2024-04-05T10:15:00Z"
  }
}

When all reviewers approve, the platform transitions the service to the next stage. If a reviewer updates their status, the platform updates the approval object and sends a real-time update via WebSocket.

Observability and Incident Response

The platform provides built-in observability for all services. Each service must expose a /health endpoint and include a observability.yaml file.

# observability.yaml
tracing:
  enabled: true
  exporter:
    type: jaeger
    endpoint: http://jaeger-collector:14268/api/traces
  sampling:
    rate: 0.1
    max-connections: 100
logging:
  level: info
  format: json
  output: stdout
  retention: 7d
monitoring:
  alerts:
    - name: "High request latency"
      query: "avg(rate(http_request_duration_seconds[5m])) > 0.5"
      severity: warning
      duration: 5m
      slack: true
  metrics:
    - name: "http_requests_total"
      labels: [ "method", "status" ]
    - name: "memory_usage_bytes"
      labels: [ "instance" ]

The platform collects logs, metrics, and traces and serves them through a unified dashboard. If tracing.exporter.endpoint is unreachable, the platform logs:

ERROR: Tracing exporter failed to connect to http://jaeger-collector:14268/api/traces
Status: 503 Service Unavailable
Response body: {"error": "Service not available"}

The platform retries for 5 minutes, then marks the service as degraded. If the endpoint remains unreachable for 10 minutes, it triggers a service-degraded alert.

When an alert fires, the platform creates an incident in the incidents table of the platform database:

INSERT INTO incidents (
  service,
  title,
  severity,
  status,
  created_at,
  last_updated
) VALUES (
  'my-new-service',
  'High request latency in production',
  'warning',
  'open',
  '2024-04-05T10:20:00Z',
  '2024-04-05T10:20:00Z'
);

The incident includes a link to the service’s dashboard, a summary of the alert, and a timeline of recent events.

Developer Experience and Feedback

The platform collects feedback from developers through a feedback.yaml file and a structured API.

# feedback.yaml
categories:
  - name: "Onboarding"
    description: "Experience when joining the platform"
    priority: high
    questions:
      - question: "How easy was it to register your service?"
        type: scale
        min: 1
        max: 5
        default: 3
      - question: "What was missing or confusing?"
        type: text
        required: true
  - name: "Deployment"
    description: "Feedback on deployment process"
    priority: medium
    questions:
      - question: "How long did it take to deploy a new version?"
        type: time
        unit: minutes
      - question: "What would improve the deployment workflow?"
        type: text

The platform surfaces a feedback survey via a GET /api/feedback/for-service/{service} endpoint. The response is a JSON object with id, service, survey, and questions.

When a developer submits feedback, the platform stores it in a feedback_submissions table:

CREATE TABLE feedback_submissions (
  id UUID PRIMARY KEY,
  service TEXT NOT NULL,
  survey_id UUID NOT NULL,
  submitted_at TIMESTAMP WITH TIME ZONE,
  answers JSONB
);

The platform uses answers as a JSONB column. It supports queries like:

SELECT service, AVG(answers->'onboarding'->'how-easy') AS avg_rating
FROM feedback_submissions
WHERE submitted_at > '2024-04-01T00:00:00Z'
GROUP BY service;

The platform also supports feedback.yaml being updated dynamically. If the file changes after a submission, the platform updates the survey and marks all existing submissions as outdated.

Platform as a Product: Silent Failures

The following are common points of failure that go unnoticed unless monitored:

  WARNING: Invalid rollout interval format in deploy.yaml: expected duration (e.g., 30s, 2m), got 30
  ERROR: Service my-new-service not found in platform registry
  Suggestion: Run `git add services/my-new-service && git commit -m "Register service" && git push`
  CRITICAL: Service my-new-service: /health response took 12.3s, exceeding threshold of 10s
  WARNING: CI worker image not specified in ci-worker.yaml. Using default image: ubuntu:22.04.

Platform as a Product: The Core Pattern

Platform as a Product is defined by a few key patterns:

The platform is not a collection of tools; it is a product that evolves with its users. Every change to a manifest.json, every deploy.yaml, every feedback.yaml, is a product decision. The platform is not just a stack—it is a contract between engineering teams, and it is maintained as a product in its own right.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.