GraphQL Federation: Schemas aufteilen, ohne Teams zu spalten
Ein praktischer Leitfaden zu GraphQL Federation: ein monolithisches Schema auf Services aufteilen, ohne Resolver oder Gateway-Logik zu duplizieren.

Jeder GraphQL-Monolith stößt irgendwann an dieselbe Wand. Ein Schema, ein Repo, eine Deploy-Pipeline — und am Ende blockiert ein Team jedes Release der anderen, weil sein Resolver einen Typ berührt hat, von dem alle abhängen. Der naheliegende Reflex ist, das Schema so aufzuteilen wie man eine REST-API aufteilen würde — entlang von Service-Grenzen. Aber GraphQL-Clients erwarten einen einzigen Graphen, nicht fünf. Genau dieses Problem löst GraphQL Federation: Mehrere Teams besitzen unabhängige Subgraphen, und ein Gateway setzt sie zu einem einzigen abfragbaren Supergraph zusammen — ohne dass jemand Stitching-Logik von Hand schreibt.
Das ist keine generelle Empfehlung für Microservices. Es ist ein schmales Werkzeug für ein schmales Problem: Ihre Service-Grenzen existieren bereits, und der Graph soll sie widerspiegeln, ohne dass Clients davon überhaupt wissen müssen.
Das Problem mit einem einzigen Schema
Ein monolithischer GraphQL-Server zentralisiert die Eigentümerschaft über jeden Typ. User, Order, Product und Inventory leben alle in einer Codebasis, aufgelöst von einem einzigen Prozess. Bei zehn Typen ist das kein Problem. Bei zweihundert Typen, mit fünf Teams, die in dieselbe schema.graphql committen, bekommt man:
- Merge-Konflikte bei gemeinsamen Typen, die niemandem klar gehören
- Einen Single Point of Failure — ein schlechtes Resolver-Deployment legt den gesamten Graphen lahm
- Keine Möglichkeit, den
Inventory-Service unabhängig von den Auth-Checks inUserzu skalieren
Die naive Lösung — Schema Stitching — brauchte einen koordinierenden Prozess, der Remote-Schemas manuell zusammenführte und Queries umschrieb. Das funktionierte, aber jeder neue Subgraph brauchte handgeschriebene Merge-Logik. Federation, wie es die Federation-Spezifikation von Apollo definiert, ersetzt dieses manuelle Zusammenführen durch einen deklarativen Kompositionsschritt: Jeder Subgraph erklärt, was er besitzt, und ein Kompositions-Tool baut daraus automatisch den Supergraph.
Wie ein Subgraph Eigentümerschaft erklärt
Jeder Service liefert ein normales GraphQL-Schema, angereichert mit Federation-Direktiven. Die wichtigste ist @key, die dem Gateway sagt, wie eine Entität über Subgraphen hinweg identifiziert wird.
# subgraph: products
type Product @key(fields: "id") {
id: ID!
name: String!
priceCents: Int!
}
# subgraph: inventory
type Product @key(fields: "id") {
id: ID! @external
stockCount: Int!
warehouseId: ID!
}
extend type Query {
lowStockProducts(threshold: Int!): [Product!]!
}Beide Services definieren Product, aber keiner besitzt den gesamten Typ. products besitzt name und priceCents; inventory besitzt stockCount. Die Direktive @key(fields: "id") teilt der Kompositionsschicht mit, dass es sich um dieselbe Entität handelt, verbunden über id. Keine gemeinsame Datenbank, keine gemeinsame Codebasis — nur ein gemeinsamer Identifier.
Referenzen über Services hinweg auflösen
Wenn eine Query stockCount auf einem Product anfragt, das ursprünglich aus dem products-Subgraph stammt, muss der Gateway den inventory-Subgraph zur Auflösung heranziehen. Dafür ist __resolveReference da:
// inventory subgraph resolver
const resolvers = {
Product: {
async __resolveReference(reference: { id: string }) {
// ❌ Bad: N+1 query per product, one at a time
// return db.inventory.findOne({ productId: reference.id });
// ✅ Good: batch reference resolution with a dataloader
return inventoryLoader.load(reference.id);
},
},
Query: {
async lowStockProducts(_: unknown, args: { threshold: number }) {
const rows = await db.inventory.find({
stockCount: { $lt: args.threshold },
});
return rows.map((row) => ({ id: row.productId }));
},
},
};Der Gateway bündelt alle __resolveReference-Aufrufe für einen gegebenen Subgraph zu einer einzigen Anfrage pro Query — das läuft also einmal pro Query, nicht einmal pro Entität. Aber nur, wenn man auch innerhalb des Resolvers batcht. Lässt man den Dataloader weg, hat man das N+1-Problem lediglich vom Gateway in den Subgraph verschoben.
__resolveReference wird standardmäßig einmal pro Entität pro Query aufgerufen. Ohne Batching löst eine Query mit 50 Produkten 50 Roundtrips zum Inventory-Subgraph aus — obwohl Federation selbst auf Netzwerkebene bereits bündelt.
Den Supergraph komponieren und ausliefern
Der Gateway führt Schemas zur Laufzeit nicht per Rateversuch zusammen — er konsumiert ein vorab berechnetes Supergraph-Schema, das ein Kompositionsschritt erzeugt und dabei validiert, dass sich alle Subgraphen bei gemeinsamen Typen einig sind.
// gateway.ts
import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway";
import { ApolloServer } from "@apollo/server";
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "products", url: "http://products.internal:4001/graphql" },
{ name: "inventory", url: "http://inventory.internal:4002/graphql" },
{ name: "orders", url: "http://orders.internal:4003/graphql" },
],
}),
});
const server = new ApolloServer({ gateway });IntrospectAndCompose ist praktisch für die Entwicklung, komponiert aber bei jedem Gateway-Neustart neu, indem es live gegen die Subgraphen fragt — in Produktion keine gute Idee, wo ein kurzzeitig nicht erreichbarer Subgraph nicht gleich den ganzen Graphen lahmlegen sollte. Produktions-Setups veröffentlichen stattdessen ein fertig komponiertes Supergraph-SDL in einer Schema-Registry, und der Gateway lädt ein statisches, bereits validiertes Artefakt.
# supergraph-config.yaml — used by rover to compose ahead of deploy time
federation_version: 2
subgraphs:
products:
routing_url: https://products.internal/graphql
schema:
file: ./products/schema.graphql
inventory:
routing_url: https://inventory.internal/graphql
schema:
file: ./inventory/schema.graphqlDie Komposition passiert in der CI, gegen Schema-Dateien, nicht gegen Live-Traffic. Ein kaputtes Subgraph-Schema lässt den Build fehlschlagen, bevor es überhaupt beim Gateway ankommt. Das ist dieselbe Disziplin, die man sich für jedes über Teams geteilte GraphQL-Schema wünscht — den Vertrag validieren, bevor er ausgeliefert wird, nicht danach.
Einen Monolithen schrittweise migrieren
Man muss nicht am ersten Tag alles federieren. Der praktikable Weg orientiert sich am Strangler-Fig-Pattern: Den bestehenden Monolithen als einzelnen Subgraph einwickeln und dann Typ für Typ herauslösen.
# legacy-monolith subgraph — everything not yet extracted
extend type Query {
legacyUsers: [User!]!
}
type User @key(fields: "id") {
id: ID!
email: String!
# ownership will move to a dedicated `identity` subgraph later
}Dem Gateway ist egal, dass ein "Subgraph" in Wirklichkeit der gesamte alte Server ist. Clients sehen die ganze Zeit über einen einzigen Graphen; die Migration bleibt für sie unsichtbar. Genau das ist der eigentliche Vorteil von Federation gegenüber einem Rewrite — sie entkoppelt das Organisationsproblem vom API-Vertragsproblem.
Die Kosten der Query-Planung
Jede federierte Query wird vor der Ausführung geplant: Der Gateway ermittelt, welche Subgraphen in welcher Reihenfolge angefragt werden müssen und wie die Ergebnisse zusammengefügt werden. Diese Planung hat reale Latenzkosten, die mit der Anzahl der Subgraphen wachsen.
| Betroffene Subgraphen | Typische zusätzliche Latenz | Gegenmaßnahme |
|---|---|---|
| 1–2 | vernachlässigbar | keine nötig |
| 3–5 | 5–20 ms | Query-Plan-Caching |
| 6+ | 20 ms+ | Entity-Fan-out reduzieren, Hot Paths kolokalisieren |
Query-Plan-Caching (mit dem normalisierten Query-String der Operation als Key) eliminiert die Neuplanungskosten bei wiederholten Queries — und deckt damit den Großteil des Produktions-Traffics ab, da Clients typischerweise nur eine kleine, stabile Menge an Operationen senden.
Die wichtigsten Punkte
- Federation löst ein organisatorisches Problem — unabhängige Team-Ownership — kein Performance-Problem. Nicht nur greifen, um Dateien aufzuteilen.
@key-Direktiven definieren die Entitätsidentität über Subgraphen hinweg; liegt man hier falsch, schlägt die Komposition entweder fehl oder erzeugt still einen falschen Join.__resolveReferencemit einem Dataloader batchen — Federation batcht auf Netzwerkebene, nicht innerhalb des Resolvers.- Den Supergraph in CI gegen Schema-Dateien komponieren, nicht beim Gateway-Start gegen Live-Traffic.
- Schrittweise migrieren, indem der bestehende Monolith als ein Subgraph eingewickelt und Typen nach und nach herausgelöst werden.
- Query-Pläne cachen; identische Operationen bei jedem Request neu zu planen ist verschwendete Latenz, die man sich sparen kann.


