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.
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.
replicas in 1.0 now expects replicationCount in 1.5. The schema is not versioned, so older clients fail silently.tags: services are tagged inconsistently (e.g., database, Database, DB, relational-db). A query on tag=database misses services tagged DB.POST /provision payload is missing database, but the catalog returns a 400 Bad Request with:{
"error": "Validation failed",
"details": [
{
"field": "database",
"message": "Required property is missing"
}
]
}
No 422 Unprocessable Entity response, no validationErrors array—just a plain details list.
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:
serviceIdparameterscontextstatus (pending, running, succeeded, failed)createdAt, startedAt, completedAterror (if status is failed)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.
running. The task remains pending forever.context.projectId is not used by the provisioning system, but the service expects it. A service instance is created, but no project association exists.replicas in the catalog is mapped to replicationCount in the actual provisioning API, but the mapping is not documented. The replicas field is ignored.failed with a 504 Gateway Timeout from the provisioning system, but the task continues in the background.When the provisioning API returns, the catalog updates the task status and triggers a PUT /tasks/{taskId} to notify the developer.
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}.
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.
failed-webhooks table.ready to upgrading, but the catalog does not set startedAt. The updatedAt field is used for all state changes, but startedAt is not updated when the state changes.1.0 and 1.2. A new provisioned instance uses 1.0, but the catalog’s schema field is from 1.2. The parameters are validated against 1.2, but the provisioning API expects 1.0 inputs. The catalog does not track which version a service instance uses.context.projectId is stored in the task, but the service instance metadata is stored separately. The catalog does not synchronize them automatically.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.
backupRetention in version 1.1. The catalog validates incoming parameters against the versioned schema, but does not require backupRetention. It is optional.1.0 schema is upgraded to 1.1. The catalog must populate backupRetention from defaults or a migration script.backupRetention in 1.1, but the provisioning API returns backupRetentionDays. The catalog does not map backupRetentionDays to backupRetention automatically.The catalog exposes a GET /contracts/{serviceId}/{version} endpoint to retrieve the contract for a specific version.
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:
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:
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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy