Declarative Service Composition
Connect services via configuration, not code. Declare a remote API — routes automatically proxy, contracts merge, authentication flows through. Change a URL in config, the service reconnects.
Modern applications are compositions of services. Authentication, payments, email, storage, analytics — each a separate service, each requiring integration code. What if connecting services was as simple as declaring them in configuration?
The Integration Tax
Every service integration follows a similar pattern:
- Read the API documentation
- Install an SDK (or write raw HTTP calls)
- Configure authentication
- Write wrapper functions for the endpoints you need
- Handle errors, retries, timeouts
- Write tests mocking the external service
- Keep the integration updated as the API evolves
For one service, this is manageable. For ten services, it's a significant portion of your codebase. For fifty services — the reality of enterprise systems — integration code dominates development effort.
Declarative service composition eliminates this tax. You declare what services you're connecting to. The platform handles the how.
Real-World Use Cases
API Gateway Pattern
Your frontend needs to call five backend services. Instead of making five direct connections, you declare all five in your gateway's config. The gateway exposes a unified API, proxies requests appropriately, merges authentication, and handles failures. Add a new backend service? Add a line to config.
Service Fragmentation
Your monolithic API is getting too large. You want to extract the reporting endpoints to a separate service for independent scaling. Declare the split in configuration — reporting requests route to the new service, everything else stays. The external API contract doesn't change.
Environment-Specific Routing
In development, payments go to Stripe's test mode. In staging, they go to a mock service for integration testing. In production, they go to live Stripe. Same code, different config URLs. The application logic doesn't know or care.
Execution Profile Fragmentation
Some endpoints are fast (milliseconds) — perfect for Lambda. Others are slow (minutes) — need long-running EC2 instances. Fragment your API by execution profile in configuration, route accordingly, and re-merge into a unified external API.
How Existing Tools Approach This
Kong / NGINX
API gateways that handle routing, rate limiting, and authentication. Configuration-driven but focused on infrastructure concerns. The application still needs SDK code for each upstream service's specific API.
Apollo Federation
Composes multiple GraphQL services into a unified schema. Declarative composition through schema stitching. Powerful, but specific to GraphQL — REST services require a different approach.
AWS AppSync
Managed GraphQL that connects to various data sources through declarative resolvers. You declare the data source, AppSync handles the connection. Tightly coupled to AWS ecosystem.
Zapier / n8n / Make
Visual workflow builders that connect services without code. Declarative, but designed for simple automations rather than building unified APIs. Great for operations; not a development platform.
Spring Cloud Gateway
Java-based gateway with declarative route configuration. Combines routing rules with predicates and filters in YAML. Powerful for Java ecosystems; requires Spring expertise.
Backstage Service Catalog
Spotify's platform for documenting and discovering services. Declarative service definitions in YAML, but focused on documentation and discovery rather than runtime composition.
How It Works
In an Application Operating System, service composition happens through configuration. First, wrap an external API as a service:
// intdb-sharepoint/config.json — Wrapping Microsoft SharePoint API
{
"databases": {
"intdb_sharepoint": {
"tables": {
"sites": { "$aHCoreTables": "jsen-sym-schema-for-sites" },
"drives": { "$aHCoreTables": "jsen-sym-schema-for-drives" },
"items": { "$aHCoreTables": "jsen-sym-schema-for-items" },
"lists": { "$aHCoreTables": "jsen-sym-schema-for-lists" }
}
}
},
"routes": {
"$db:list.GET:/intdb_sharepoint.sites": {
"path": ["plg-list-get-sites"], // Calls SharePoint Graph API
"expects": { ... },
"returns": { ... }
},
"$db:list.GET:/intdb_sharepoint.sites,intdb_sharepoint.drives": {
"path": ["plg-list-get-drives"], // Nested resource traversal
"expects": { ... },
"returns": { ... }
}
}
}
The wrapper service translates external API calls into a standardized interface. Custom plugins handle the actual Microsoft Graph API calls — consumers never know SharePoint is underneath.
Now declare the wrapped service as a remote API:
// apidb/config.json — Importing the wrapped SharePoint service
{
"apis-remote": {
"intdb_sharepoint": {
"deployments": {
"development": {
"url": "{{ DEF_SERVICES_URL_FOR_APIS_REMOTE_INTDB_SHAREPOINT }}"
},
"production": {
"url": "https://sharepoint-wrapper.internal.company.com"
}
}
},
"identityapi": {
"deployments": {
"development": { "url": "{{ DEF_SERVICES_URL_FOR_APIS_REMOTE_IDENTITYAPI }}" },
"production": { "url": "https://auth.internal.company.com" }
}
}
}
}
Import routes from remote APIs using the $api: notation:
// apidb/config.json — Importing routes from remote services
{
"routes": {
// Import SharePoint routes, add authentication layer
"$api:intdb_sharepoint.list.GET:/intdb_sharepoint.sites": {
"path": [
"plg-auth-get-member", // Local auth plugin
"plg-auth-integration" // Then proxy to remote
],
"expects": {
"kSType": "J",
"kJKeys": {
// Inherit remote contract, extend with local fields
"$api:intdb_sharepoint.list.GET:/intdb_sharepoint.sites -> expects": "",
"organization_workspace_id": { "kSType": "S" }
}
},
// Inherit the return contract directly
"returns": "$api:intdb_sharepoint.list.GET:/intdb_sharepoint.sites -> returns"
},
// Import auth routes from identity service
"$api:identityapi.list.PATCH:/identitydb.identities/activate": {
"path": ["jsen-sym-plugin-default-connect"]
}
}
}
This configuration tells the platform:
- Where each service is — URLs per deployment environment, environment-variable based
- What routes to import — The
$api:prefix imports remote routes - How contracts merge —
-> expectsand-> returnsinherit remote contracts - What plugins to inject — Add authentication before proxying
"Connect services via configuration, not code. Declare a remote API in config — routes automatically proxy, contracts merge, authentication flows through."
Contract Merging
When you import a route with $api:service.route -> expects, its contract merges with your API's contract. You can extend the inherited schema with additional fields while preserving the original contract. The notation -> returns inherits return types directly.
If the remote service updates its contract, the import notation ensures type compatibility — your local extensions merge cleanly with the upstream changes.
Authentication Flow-Through
Authentication flows through the composition by plugin order. When importing a route, you prepend authentication plugins before the proxy plugin. The request authenticates locally, then forwards to the remote service with credentials attached.
Change authentication providers? Update the plugin in your route definition. The wrapped service remains unchanged — it just receives authenticated requests.
Why This Matters
Modern applications aren't monoliths — they're compositions. But current tooling makes composition expensive:
- Integration code that dwarfs business logic
- Inconsistent error handling across services
- Authentication complexity multiplying with each service
- Contract drift causing runtime failures
- Operational overhead of managing connections
Declarative composition inverts the cost model. Adding a service becomes a configuration change, not a development project. Contracts are enforced at compile time. Authentication is centralized. Error handling is consistent.
The Operating System Parallel
Operating systems compose services declaratively. You don't write code to "connect" to the filesystem — you declare a path and the kernel routes to the appropriate driver. Network connections? Declare an address; the TCP/IP stack handles it. Device access? Open a handle; the HAL routes appropriately.
An application operating system extends the same model to service integration. Declare what you're connecting to. Let the platform handle the how.