Service Catalog Design Patterns

Design and operate an internal service catalog that gives developers and platform teams visibility into every service, its ownership, health, and dependencies. Covers catalog data models, ownership enforcement, health scoring, Backstage integration, and service maturity frameworks.

A service catalog is a curated, discoverable inventory of reusable services, APIs, and infrastructure components offered by a platform team to developers. It matters when teams need to consistently adopt, integrate, and maintain services across a growing number of applications, especially when multiple teams, domains, and environments coexist. This page documents specific, repeatable approaches to designing a service catalog that scales with complexity and developer velocity.

Discovering the Right Service

When a developer needs a service, they start with a question: *What does the platform offer, and how do I find the right one?* The discovery process must be fast, predictable, and precise.

A service catalog should expose metadata that enables filtering, searching, and comparison. The primary entry point is a /services endpoint returning a list of services in JSON:

[
  {
    "id": "db-postgres-1",
    "name": "PostgreSQL Cluster",
    "description": "High-availability PostgreSQL 15 with replication and backup",
    "version": "15.3",
    "provider": "platform-team",
    "tags": ["database", "production", "managed"],
    "documentation": "https://docs.example.com/services/postgres",
    "api": {
      "url": "https://api.example.com/v1/postgres",
      "auth": "oauth2",
      "schema": "https://api.example.com/v1/postgres/schema.json"
    },
    "status": "active",
    "created": "2023-06-15T10:00:00Z"
  }
]

The catalog should support filtering via query parameters. For example, to find all database services with a specific version:

GET /services?tags=database&version=15.3

A developer can also retrieve a single service by ID:

GET /services/db-postgres-1

The response includes a full specification, including required configuration parameters, example use cases, and expected performance guarantees.

Onboarding with a Standardized Interface

Once a developer selects a service, they need to integrate it into their application. The onboarding process must reduce cognitive load and eliminate common configuration errors.

Each service defines a spec object in its metadata, describing the parameters required to instantiate it. This spec is a JSON schema that can be validated against a known format.

For example, the PostgreSQL service's spec:

{
  "type": "object",
  "properties": {
    "instanceType": {
      "type": "string",
      "enum": ["small", "medium", "large", "xlarge"],
      "default": "medium"
    },
    "backupRetentionDays": {
      "type": "integer",
      "minimum": 7,
      "maximum": 365,
      "default": 30
    },
    "enableMonitoring": {
      "type": "boolean",
      "default": true
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "default": []
    }
  },
  "required": ["instanceType"]
}

When a developer creates a new instance of the service, they submit a create request to the catalog’s /instances endpoint:

POST /instances
{
  "serviceId": "db-postgres-1",
  "name": "user-profile-db",
  "parameters": {
    "instanceType": "large",
    "backupRetentionDays": 90,
    "enableMonitoring": true,
    "tags": ["user", "profile", "production"]
  }
}

The catalog validates the parameters against the spec schema. If validation fails, the response includes a list of errors:

{
  "errors": [
    {
      "field": "parameters.backupRetentionDays",
      "message": "Must be at least 7 days"
    },
    {
      "field": "parameters.instanceType",
      "message": "Must be one of: small, medium, large, xlarge"
    }
  ],
  "status": "invalid"
}

The most common failure is misalignment between the service definition and the instance configuration. Developers often omit required fields or use incorrect types. The catalog must return clear, actionable errors with field-level details.

Versioning and Dependency Management

As services evolve, multiple versions must coexist. A developer must be able to select a specific version of a service, and the catalog must handle versioning transparently.

Each service has a version field, and versions are tied to a release tag in the service’s Git repository. The catalog tracks a version object for each service, including:

When a service is updated, the platform team pushes a new version and updates the catalog via a PUT /services/db-postgres-1/version/15.3 request:

{
  "version": "15.3",
  "releaseNotes": "Added logical replication and improved WAL tuning.",
  "schema": "https://api.example.com/v1/postgres/schema-15.3.json",
  "changelog": [
    {
      "type": "feature",
      "title": "Logical Replication Support",
      "description": "Enable logical replication between clusters."
    },
    {
      "type": "bugfix",
      "title": "WAL Compression",
      "description": "Improved WAL compression efficiency by 30%."
    }
  ],
  "status": "active"
}

The catalog must resolve the correct spec schema based on the version. If a service instance references a version that is not present in the catalog, the instance creation fails with a clear error:

{
  "error": "Service not found",
  "details": {
    "serviceId": "db-postgres-1",
    "version": "16.0",
    "availableVersions": ["15.1", "15.2", "15.3"]
  },
  "status": "not-found"
}

A subtle but critical failure mode is version drift: a service instance is created using version 15.3, but the catalog later updates the spec for 15.3 without incrementing the version. Developers may expect new fields to be available, but the instance continues to use the old schema.

Automating Service Lifecycle

The service catalog is not just a discovery layer—it must drive the lifecycle of services from provisioning to decommissioning.

Provisioning is triggered by a create request and managed by a workflow engine. The system uses a service object to define a lifecycle of steps:

{
  "serviceId": "db-postgres-1",
  "lifecycle": {
    "create": [
      {
        "step": "provision-vm",
        "type": "kubernetes",
        "template": "postgres-cluster.yaml",
        "parameters": {
          "instanceType": "instanceType",
          "backupRetentionDays": "backupRetentionDays"
        }
      },
      {
        "step": "apply-database-schema",
        "type": "sql",
        "source": "https://example.com/schemas/user-profile.sql"
      },
      {
        "step": "enable-monitoring",
        "type": "webhook",
        "url": "https://monitoring.example.com/api/enable",
        "payload": {
          "serviceId": "db-postgres-1",
          "instanceId": "{{instanceId}}"
        }
      }
    ],
    "update": [
      {
        "step": "resize-cluster",
        "type": "kubernetes",
        "template": "resize-postgres.yaml"
      },
      {
        "step": "migrate-schema",
        "type": "sql",
        "source": "https://example.com/schemas/migration-15.3-to-15.4.sql"
      }
    ],
    "delete": [
      {
        "step": "backup-data",
        "type": "script",
        "command": "pg_dump -h $HOST -U $USER $DB_NAME > /backups/$(date +%Y-%m-%d).sql"
      },
      {
        "step": "notify-team",
        "type": "email",
        "to": "[email protected]",
        "subject": "PostgreSQL instance deleted: {{instanceId}}"
      }
    ]
  }
}

Each step in the lifecycle can be executed by a different backend system. The type field determines the runner: kubernetes, sql, webhook, script, or http.

The catalog orchestrates these steps. When a new instance is created, it executes the create steps in order. If one step fails, the catalog logs the error and halts execution, recording the failure in the instance’s status field.

A common failure is that the provision-vm step returns a 201 Created but the instanceId is not available in the response body. The catalog expects the instanceId to be in the Location header, but the VM provisioner returns it in the response body under id. The resulting error is a missing instanceId field in the instance metadata.

Consistent Metadata and Extensibility

The service catalog must support rich, standardized metadata so that services can be compared, filtered, and extended.

Every service includes a metadata object that defines its properties:

"metadata": {
  "maintainer": "[email protected]",
  "cost": {
    "monthly": 1200,
    "perInstance": 200,
    "currency": "USD"
  },
  "uptimeSla": "99.9%",
  "supportLevel": "standard",
  "suggestedFor": ["microservices", "event-driven", "batch-processing"]
}

The supportLevel field can be used to categorize services by support tier:

A developer can filter services by supportLevel:

GET /services?supportLevel=premium

The catalog also allows for extensibility via extensions, a map of custom fields that can vary per service:

"extensions": {
  "db-postgres-1": {
    "backupFrequency": "daily",
    "replicationMode": "synchronous"
  },
  "cache-redis-1": {
    "evictionPolicy": "lru",
    "maxMemory": "10GB"
  }
}

Extensions are stored in a separate table or as JSON in a database. The catalog exposes them via a GET /services/{id}/extensions endpoint.

Managing Dependencies and Interactions

A service may depend on another service. The catalog must surface these dependencies and enforce consistency.

A service can declare dependencies in its spec:

"dependencies": [
  {
    "serviceId": "auth-oidc-1",
    "version": "2.0",
    "required": true,
    "description": "User authentication via OIDC."
  },
  {
    "serviceId": "message-broker-1",
    "version": "1.5",
    "required": false,
    "description": "Optional event publishing."
  }
]

When a service instance is created, the catalog validates that all required dependencies are available and compatible. If a required dependency is not found, the instance creation fails with:

{
  "error": "Dependency not satisfied",
  "details": {
    "missingDependencies": [
      {
        "serviceId": "auth-oidc-1",
        "version": "2.0",
        "availableVersions": ["1.8", "1.9", "2.1"]
      }
    ]
  },
  "status": "dependency-missing"
}

The catalog can also compute a dependency graph for a service instance. For example, when a developer creates an instance of a user-service, the catalog resolves all dependencies and returns a full graph:

GET /instances/user-service-1/dependencies
{
  "instanceId": "user-service-1",
  "services": [
    {
      "serviceId": "user-service",
      "version": "1.2",
      "dependencies": [
        {
          "serviceId": "db-postgres-1",
          "version": "15.3",
          "status": "active"
        },
        {
          "serviceId": "auth-oidc-1",
          "version": "2.0",
          "status": "active"
        }
      ]
    }
  ]
}

This graph enables a developer to trace how a service is composed and to identify bottlenecks or misconfigurations.

Summary of Key Failures and Gotchas

The service catalog is not a static list; it is a living system that evolves with the platform. Its success depends on consistency, clarity, and the ability to handle the details that make developers productive.

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.