Service Catalog Engineering

Production engineering guide for service catalog engineering covering patterns, implementation strategies, and operational best practices.

Service catalogs are the central registry of services that developers can discover, provision, and operate. They matter when engineering teams scale beyond a handful of services, and when teams must collaborate across boundaries—infrastructure, platform, and application—without tight coupling. A well-engineered service catalog enables developers to self-serve infrastructure, understand service contracts, and integrate new components with confidence. It is the foundation of a platform that truly empowers the developer.

Discovering a Service

A developer begins with a question: *What services are available, and how do I use them?* The service catalog answers this through a standardized interface, typically accessible via a web portal or API. The primary discovery mechanism is a search endpoint: GET /services?q=database returns a paginated list of services matching the query.

GET /services?q=database&limit=10&offset=0

Response:

{
  "data": [
    {
      "id": "db-postgres-1",
      "name": "PostgreSQL 15 Cluster",
      "description": "High-availability PostgreSQL cluster with automatic failover and backup.",
      "version": "15.2",
      "maintainer": "[email protected]",
      "tags": ["database", "relational", "production"],
      "metadata": {
        "apiEndpoint": "https://api.db.acme.com/v1",
        "docsUrl": "https://docs.acme.com/db/postgres",
        "support": {
          "slack": "#db-support",
          "email": "[email protected]"
        }
      },
      "schema": {
        "type": "object",
        "properties": {
          "database": { "type": "string" },
          "username": { "type": "string" },
          "password": { "type": "string" },
          "replicas": { "type": "integer", "minimum": 1, "default": 1 }
        },
        "required": ["database"]
      }
    }
  ],
  "pagination": {
    "total": 42,
    "limit": 10,
    "offset": 0,
    "next": "/services?q=database&limit=10&offset=10"
  }
}

Each service entry includes a schema field that defines the inputs required to provision the service. The schema is a JSON Schema object, and it is validated before a service is provisioned.

When a developer clicks on a service, they see a page with metadata, usage examples, and a “Provision” button. The button triggers a POST /provision with a payload matching the service’s schema. The catalog then returns a 202 Accepted response with a Location header pointing to a task tracking the provisioning.

Common Discovery Pitfalls

{
  "error": "Validation failed",
  "details": [
    {
      "field": "database",
      "message": "Required property is missing"
    }
  ]
}

No 422 Unprocessable Entity response, no validationErrors array—just a plain details list.

Provisioning a Service

After discovery, the developer provisions a service instance. The catalog uses a provisioning workflow that consists of three phases: validation, execution, and completion.

The request starts with a POST /provision:

POST /provision
Content-Type: application/json

{
  "serviceId": "db-postgres-1",
  "parameters": {
    "database": "user-data",
    "username": "app-user",
    "password": "secure-pass-123",
    "replicas": 3
  },
  "context": {
    "projectId": "project-123",
    "team": "frontend-team",
    "environment": "staging"
  }
}

The service catalog validates the parameters against the service’s schema. If validation fails, it returns a 400 Bad Request and stops.

If validation passes, the catalog creates a provisioning-task record in a database, with a unique taskId. It then queues the task using a message broker (e.g., Kafka, RabbitMQ, or a simple tasks table in PostgreSQL). The task includes:

The catalog runs a background worker that polls for tasks and executes them. The worker calls an external provisioning API (e.g., POST /api/v1/provision on a service registry) with the full task payload.

Provisions Can Fail Silently

When the provisioning API returns, the catalog updates the task status and triggers a PUT /tasks/{taskId} to notify the developer.

Example: Provisioning Task Update

PUT /tasks/db-postgres-1-42
Content-Type: application/json

{
  "status": "succeeded",
  "result": {
    "instanceId": "pg-12345",
    "url": "https://user-data.db.acme.com",
    "credentials": {
      "host": "pg-12345.acme.com",
      "port": 5432,
      "database": "user-data",
      "username": "app-user",
      "password": "secure-pass-123"
    },
    "status": "ready"
  },
  "updatedAt": "2024-08-15T10:30:00Z"
}

The catalog stores the result and exposes it via a new endpoint: GET /provisioned/{instanceId}.

Managing Service Lifecycle

Once a service is provisioned, the catalog tracks its lifecycle. Each service instance has a state machine: created, provisioning, ready, upgrading, degraded, failed, deleted.

The catalog exposes lifecycle events via a webhook system. When a service instance changes state, the catalog sends a POST to a configured webhookUrl:

{
  "event": "service.state.changed",
  "timestamp": "2024-08-15T10:31:15Z",
  "data": {
    "instanceId": "pg-12345",
    "oldState": "provisioning",
    "newState": "ready",
    "serviceId": "db-postgres-1",
    "metadata": {
      "environment": "staging",
      "project": "project-123"
    }
  }
}

Webhooks are configured via a PATCH /services/{serviceId}/webhooks endpoint:

PATCH /services/db-postgres-1/webhooks
Content-Type: application/json

[
  {
    "name": "notify-deployments",
    "url": "https://deployments.acme.com/webhook/service-updated",
    "events": ["service.state.changed", "service.version.changed"],
    "filters": {
      "environment": ["staging", "production"],
      "version": ">=1.2.0"
    },
    "headers": {
      "Authorization": "Bearer abc123",
      "Content-Type": "application/json"
    }
  }
]

The catalog supports filtering by event type, service metadata, and version. A webhook is only triggered if all filters match.

Lifecycle Management Gotchas

Discovering and Using Service Contracts

A service catalog is not just a list of services—it is a contract layer. Each service defines its contract in the schema field, which includes:

The contract is used to generate documentation, client libraries, and API clients.

For example, a service with a schema that defines database, username, and password as inputs, and url, credentials, and status as outputs, allows the catalog to generate:

curl -X POST https://catalog.acme.com/provision \
  -H "Content-Type: application/json" \
  -d '{
    "serviceId": "db-postgres-1",
    "parameters": {
      "database": "user-data",
      "username": "app-user",
      "password": "secure-pass-123",
      "replicas": 3
    }
  }'

The catalog also supports contract versioning. When a service evolves, a new version is published. The catalog tracks which version is used by which instance.

Contract Validation and Evolution

The catalog exposes a GET /contracts/{serviceId}/{version} endpoint to retrieve the contract for a specific version.

Self-Service Integration

The catalog enables self-service through templates and workflows.

A template is a predefined sequence of provisioning steps. For example, a Full Stack App template includes:

  1. Provision a PostgreSQL database.
  2. Provision a Redis instance.
  3. Create a Kubernetes namespace.
  4. Deploy a frontend and backend service.

The template is defined as a JSON document:

{
  "name": "Full Stack App",
  "description": "Sets up a production-ready app with database, cache, and services.",
  "steps": [
    {
      "type": "provision",
      "serviceId": "db-postgres-1",
      "parameters": {
        "database": "{{projectName}}-db",
        "replicas": 2
      }
    },
    {
      "type": "provision",
      "serviceId": "cache-redis-1",
      "parameters": {
        "memorySize": "2GB",
        "persistence": true
      }
    },
    {
      "type": "k8s-deploy",
      "manifest": "deployments/app.yaml",
      "namespace": "{{projectName}}-ns"
    }
  ],
  "variables": [
    {
      "name": "projectName",
      "label": "Project Name",
      "description": "Name of the project for the app.",
      "type": "string",
      "required": true,
      "default": "my-app"
    }
  ]
}

When a developer selects the template, the catalog presents a form with projectName. After submission, it runs the steps in sequence, tracking the workflow in a workflows table.

The catalog supports workflow state and error handling:

Summary

A service catalog is a shared, evolving contract between platform and application teams. It enables discovery through standardized APIs, supports provisioning with validation and state tracking, and manages the lifecycle of service instances. It is the engine that powers self-service, and its success depends on consistent schema use, clear error reporting, and reliable event delivery. The key to a robust catalog is not the tooling, but the pattern: a uniform way to describe, provision, and manage services across teams and systems.

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.