Cross-Provider Mashup
Federation is not limited to one remote system. A single local entity can reference federated entities in different remote services, and a single $expand request fans out to both providers in parallel.
When to use this pattern
- Your app composes a workflow that spans multiple systems — e.g., product master data in one ERP, inventory in a warehouse system, pricing in a CPQ.
- You want one API surface (one OData service, one URL) that mashes them up for the client.
- Each provider stays independent; the consumer is the integration point.
The CDS model
Two remote services, declared in package.json under cds.requires, with their metadata imported as separate CSN files:
{
"cds": {
"requires": {
"ProviderService": {
"kind": "odata",
"model": "srv/external/ProviderService",
"credentials": { "url": "http://localhost:4444/odata/v4/provider" }
},
"InventoryService": {
"kind": "odata",
"model": "srv/external/InventoryService",
"credentials": { "url": "http://localhost:4445/odata/v4/inventory" }
}
}
}
}One local entity with associations to federated projections on both services:
using { ProviderService as remote } from '../srv/external/ProviderService';
using { InventoryService as inv } from '../srv/external/InventoryService';
// Local entity joining two remote providers
entity InventoryReports {
key ID : UUID;
product : Association to Products; // → ProviderService (:4444)
warehouse : Association to Warehouses; // → InventoryService (:4445)
note : String(500);
createdAt : Timestamp;
}
// Federated from provider A
@federation.delegate
entity Products as projection on remote.Products {
ID as productId,
name as productName,
category,
price as unitPrice,
currency
};
// Federated from provider B
@federation.delegate
entity Warehouses as projection on inv.Warehouses;InventoryReports gets two FK columns materialized locally: product_productId (→ ProviderService) and warehouse_ID (→ InventoryService). Neither remote service knows about the other; the consumer stitches them together.
The request
=== "HTTP"
```http
GET /consumer/InventoryReports?$expand=product,warehouse
```
=== "CQL"
```javascript
SELECT.from(InventoryReports).columns(
'*',
r => r.product('*'),
r => r.warehouse('*')
);
```
Under the hood
The plugin resolves each federated expand item against the service that owns it, independently:
Each expand is a separate cross-service expand (local → remote) batch-fetch, targeted at the correct remote service. The plugin identifies the owning service from each association's target projection (via the projection on <service>.<entity> clause) at plugin load time.
Running the demo
The repository's examples/consumer app has a runnable tile for this pattern:
- Start the providers:
npm run examples:startfrom the repo root. - Open http://localhost:4004/launchpage.html.
- Click the Inventory Reports tile to see the list-report UI populated from both providers.
Gotchas
- Failure modes are not transactional. If Provider A responds and Provider B times out, you get partial data — the plugin will return the primary local rows and whichever expand completed. Use
cds.log('cds-data-federation')output and HTTP status propagation from the adapter to detect partials. - No cross-provider
$filterpath traversal. You can filter byproduct/productName(same service as the FK) but not by a combined predicate spanning two providers — each cross-service filter resolves to one remote call, not a coordinated query. - Latency adds up. Two remote calls run in parallel, but the response is only as fast as the slowest provider. Consider caching (
cache: { ttl: ... }on either delegate) for read-heavy reference data — see First Cache. - Key collisions. If both providers happen to use the same ID space (e.g.
'P001'), nothing breaks — the FK columns are distinct (product_productIdvswarehouse_ID). Still, prefer distinct key shapes to avoid confusing responses.
See also
- Joining Local with Remote — the single-provider version of this pattern.
- First Cache — reduce round-trips for read-heavy cross-provider calls.
- Cross-Service Scenarios — especially the cross-provider cases (
B4..B6,B7..B14).