Build a service catalog that helps developers find, understand, and integrate with platform services. Covers catalog design, API documentation standards, ownership tracking, dependency mapping, and keeping the catalog from becoming another stale wiki.
A service catalog is a structured, searchable inventory of services, APIs, components, and systems offered by a platform team to internal developers. It matters when teams grow beyond a few services, when onboarding takes days, and when developers repeatedly build the same things in different ways. It is the central place where developers find, understand, and consume platform capabilities—turning a platform from a collection of tools into a cohesive, discoverable system.
When a developer needs to add authentication to their application, they open the platform’s developer portal and search for “auth.” The service catalog returns a list of relevant services, each with a name, description, tags, and a link to the full documentation. The goal is to answer: *What is this service? How do I use it? What do I need to get started?*
The catalog is built from metadata published by service providers. A service’s metadata is a JSON document that describes the service’s purpose, inputs, outputs, and usage. It is placed at a well-known endpoint—typically /.well-known/service-catalog.json—and serves as the entry point.
{
"name": "User Authentication Service",
"description": "A secure, scalable service for user sign-in, session management, and JWT token issuance.",
"version": "v2.1.3",
"maintainer": "[email protected]",
"tags": ["auth", "security", "jwt", "user"],
"documentation": "https://docs.example.com/services/auth",
"api": {
"base_url": "https://auth.example.com",
"endpoints": [
{
"path": "/api/v1/login",
"method": "POST",
"description": "Authenticate user and return JWT token.",
"request_body": {
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"password": { "type": "string", "minLength": 8 }
},
"required": ["email", "password"]
},
"response": {
"type": "object",
"properties": {
"token": { "type": "string", "format": "jwt" },
"expires_in": { "type": "integer", "unit": "seconds" }
}
}
}
]
},
"dependencies": [
"database:postgres",
"message-broker:kafka",
"cache:redis"
],
"integration": {
"example": {
"language": "Go",
"code": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/example/auth-client\"\n)\n\nfunc main() {\n\tclient := auth.NewClient(\"https://auth.example.com\")\n\t\n\ttoken, err := client.Login(context.Background(), \"[email protected]\", \"secret123\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t\n\tlog.Printf(\"Token: %s\", token.Token)\n}"
}
}
}
The service catalog consumes these documents via a periodic sync job. Each service is indexed into a search engine—typically Elasticsearch or PostgreSQL with full-text search—so that developers can find services by name, description, tags, or even API contract.
If the catalog uses Elasticsearch, the index is updated with the following command:
curl -X POST 'http://es.example.com:9200/services/_doc' \
-H 'Content-Type: application/json' \
-d @service-metadata.json
The index is queried using a simple GET request:
GET /services/_search?pretty
{
"query": {
"multi_match": {
"query": "user authentication",
"fields": ["name", "description", "tags"],
"fuzziness": "AUTO"
}
}
}
A common failure mode: services are published but not indexed. The developer searches for “auth” and sees no results. The cause is often a misconfigured sync job that fails silently—no error logs, no alerting. The sync job must log each successful update and fail with a detailed error when a service fails to index.
Once a developer selects a service, they see a detailed view: the metadata, example code, API reference, and a “Get Started” guide. The catalog presents a unified interface to the service’s documentation, which may be hosted on a separate site.
To ensure consistency, all services must include a README.md in a docs/ directory at the service’s repository root. The service catalog automatically fetches and renders this file.
## Overview
This service provides JWT-based authentication with OAuth2 support.
## Getting Started
1. Add the following to your `config.yaml`:
```yaml
auth:
service_url: https://auth.example.com
client_id: my-app-client
client_secret: abc123
go get github.com/example/auth-client
client := auth.NewClient("https://auth.example.com")
token, err := client.Login(ctx, "[email protected]", "secret123")
See the OpenAPI spec for full details.
The catalog renders this markdown using a standard Markdown processor—commonly `github-markdown`, `marked`, or `pandoc`—and exposes it as an HTML page.
A subtle failure: the `README.md` is not found, even though the service repository is correctly structured. The catalog expects a file at `docs/README.md`, but the service team placed it at `README.md` in the root. The fix is a configuration change in the catalog’s sync job:
```yaml
sync:
sources:
- type: git
repository: [email protected]:example/auth-service.git
branch: main
path: docs/README.md
fallback: README.md
The fallback key ensures that if docs/README.md is missing, the catalog uses README.md instead.
A developer clicks “Use This Service” and is guided through integration. The catalog generates a configuration snippet based on the service’s schema and the developer’s preferences.
The service’s schema is defined in JSON Schema, placed at schema/config.json in the service repository. The catalog uses this schema to validate and generate configuration.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User Authentication Configuration",
"type": "object",
"properties": {
"auth": {
"type": "object",
"properties": {
"service_url": {
"type": "string",
"format": "uri",
"default": "https://auth.example.com"
},
"client_id": {
"type": "string",
"minLength": 1
},
"client_secret": {
"type": "string",
"format": "password"
},
"enable_refresh": {
"type": "boolean",
"default": true
},
"token_ttl": {
"type": "integer",
"minimum": 300,
"unit": "seconds"
}
},
"required": ["client_id", "client_secret"]
}
},
"required": ["auth"]
}
The catalog renders a form from this schema, with input types (text, password, checkbox, number) and default values. When the developer fills it out, the catalog generates a configuration file.
auth:
service_url: https://auth.example.com
client_id: my-app-client
client_secret: abc123
enable_refresh: true
token_ttl: 3600
This configuration is then available for download or copy-paste.
A sharp edge: the catalog assumes the service supports a config.yaml file, but the service actually expects config.json. The developer integrates the service, but the config is ignored. The root cause is a mismatch between the catalog’s expected format and the service’s actual parser.
The fix is to define a supported_formats list in the service’s metadata:
{
"name": "User Authentication Service",
"supported_formats": ["yaml", "yml", "json"],
"config_schema": "schema/config.json"
}
The catalog displays a warning: “This service accepts YAML, YML, and JSON. We recommend YAML.”
The service catalog integrates with the developer’s existing tools. When a developer creates a new service in a Git repository, the platform automatically registers it with the catalog.
This is done via a CI/CD pipeline. On every git push, the service’s publish-to-catalog job runs.
# .github/workflows/publish-to-catalog.yml
name: Publish to Service Catalog
on:
push:
branches: [ main ]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and deploy service
run: |
make build
make deploy
- name: Publish to service catalog
run: |
curl -X POST \
-H "Authorization: Bearer $CATALOG_TOKEN" \
-H "Content-Type: application/json" \
-d @service-metadata.json \
https://catalog.example.com/api/services/register
The catalog.example.com/api/services/register endpoint expects a JSON payload with the service’s metadata and an optional version field.
{
"name": "User Authentication Service",
"version": "v2.1.3",
"metadata_url": "https://github.com/example/auth-service/.well-known/service-catalog.json",
"repository": "[email protected]:example/auth-service.git",
"provider": "[email protected]"
}
If the service already exists in the catalog, the endpoint updates it. If it’s new, it creates a new entry.
A silent failure: the catalog.example.com/api/services/register endpoint returns 200 OK, but the response body is ignored. The CI job logs success, but the catalog fails to index the service. The cause is a mismatch between the expected response shape and the actual response.
The expected response is:
{
"success": true,
"message": "Service registered successfully",
"service_id": "auth-12345"
}
But the catalog returns:
{
"status": "success",
"message": "Service registered successfully"
}
The CI job does not check the response body, so it assumes success. The fix is to add a step to validate the response:
curl -X POST \
-H "Authorization: Bearer $CATALOG_TOKEN" \
-H "Content-Type: application/json" \
-d @service-metadata.json \
https://catalog.example.com/api/services/register \
-w "\nResponse: %{response_code} %{response_body}\n"
The job fails when the response_body does not contain success: true.
A service may have multiple variants—different configurations, deployment environments, or versions. The catalog exposes these as distinct entries.
For example, the User Authentication Service has two variants:
https://staging.auth.example.com, with enable_refresh: falsehttps://auth.example.com, with enable_refresh: true, token_ttl: 3600Each variant is registered as a separate service in the catalog, with metadata updated to reflect the variant.
The catalog automatically discovers variants by scanning a variants/ directory in the service’s repository.
{
"name": "User Authentication Service (Staging)",
"description": "Staging instance of the user authentication service.",
"variant": "staging",
"environment": "staging",
"base_service": "auth-service",
"configuration": {
"enable_refresh": false,
"token_ttl": 1800
}
}
The catalog links the variant to its base service, allowing developers to switch between them. It also displays a comparison view: a side-by-side table of configuration values.
A common mistake: a service team adds a new variant but forgets to update the base_service field. The catalog treats the variant as independent, leading to duplicated documentation and inconsistent configuration.
The fix is to require the base_service field in the service’s metadata schema, and to validate it during sync.
When a service fails, the catalog displays error information directly in the service view.
A developer sees a 500 Internal Server Error when calling the /api/v1/login endpoint. The catalog shows the error with a stack trace, and links to the service’s logs.
The error is reported via a standard HTTP endpoint:
POST /api/v1/error-report
{
"service": "User Authentication Service",
"timestamp": "2024-04-15T10:30:00Z",
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Failed to authenticate user",
"details": {
"request_id": "req-12345",
"user_email": "[email protected]",
"error_type": "database.query_failed",
"query": "SELECT * FROM users WHERE email = ?"
},
"stack_trace": [
"at auth.LoginHandler.Handle(LoginHandler.go:42)",
"at auth.AuthService.Login(AuthService.go:23)",
"at main.main(main.go:15)"
]
}
}
The catalog consumes this report and displays it in real time. It also triggers alerts when errors exceed a threshold—e.g., more than 10 errors in 5 minutes.
A sharp edge: the error_type field is not indexed, so developers cannot search for “database.query_failed” across all services. The fix is to define a searchable_fields list in the catalog configuration:
catalog:
searchable_fields:
- "error.code"
- "error.message"
- "error.details.error_type"
- "error.details.request_id"
This ensures that errors can be filtered and queried efficiently.
A service catalog turns a platform into a discoverable, usable, and maintainable system. It reduces onboarding time, eliminates duplication, and enables consistent integration patterns. The key to success is a well-defined, standardized way to publish and consume service metadata—using widely adopted formats like JSON, YAML, and JSON Schema—and a robust, observable sync pipeline that ensures the catalog remains up to date. The catalog is not a static list; it is a living, evolving interface between platform and developers.
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