# GraphQL Federation: ein Schema aus vielen Subgraphs

URL: https://www.mikebild.dev/de/blog/graphql-federation-und-subgraphs/

```bash
$ rover supergraph compose --config ./supergraph.yaml

error[E029]: Encountered 1 build error while trying to build a supergraph.

Caused by:
    INVALID_GRAPHQL: [orders] Unknown directive "@key".
```

Diese Meldung kommt nicht aus einem laufenden Server, sondern aus einem Build-Schritt, den ein einzelner GraphQL-Service gar nicht kennt: der Komposition. Der Orders-Service benutzt `@key`, ohne die Direktive vorher importiert zu haben – und dieser eine fehlende Eintrag reicht, damit aus mehreren Schemas kein gemeinsamer Graph wird.

`@key` ist die Direktive, mit der Apollo Federation einen Typ über Service-Grenzen hinweg referenzierbar macht. Daran hängt alles Weitere: wer welchen Typ besitzt, wer welche Felder beisteuert, wer die Teile zur Antwort zusammensetzt – und die Frage, wie man ein Schema auseinanderschneidet, an dem drei Teams arbeiten, ohne dass die Clients davon etwas merken. Zu früh gezogen kostet dieser Schnitt allerdings mehr, als er einbringt; darauf komme ich am Schluss zurück.

## Das Problem: ein Schema, das niemandem mehr gehört

Am Anfang ist ein GraphQL-Schema wunderbar. Ein Team, ein Repository, ein `type Query`, alles liegt beieinander. Der Reiz von GraphQL ist ja gerade, dass Clients sich aus einem einzigen Endpoint genau die Felder holen, die sie brauchen. Verwandte Fragen habe ich in früheren Beiträgen schon behandelt – etwa wie man Listen mit [GraphQL-Paginierung](https://www.mikebild.dev/de/blog/graphql-paginierung-und-connections/) sauber ausliefert oder wie Berechtigungen über [GraphQL-Auth im Context](https://www.mikebild.dev/de/blog/graphql-auth-im-context/) durchgereicht werden.

Das Problem beginnt mit dem Wachstum. Sobald mehrere Teams an einem Schema arbeiten, wird die Datei zum Nadelöhr. Das User-Team will ein Feld an `type Order` hängen, muss dafür aber in den Code des Order-Teams greifen. Deployments koppeln sich aneinander. Ein `type` gehört plötzlich vielen, und „vielen gehören“ heißt in der Praxis: niemandem. Was im Alltag als Merge-Konflikt auftaucht, ist ein Zuständigkeitskonflikt.

Federation dreht die Frage um. Statt „Wie teilen sich Teams ein Schema?“ fragt sie: „Wie betreibt jedes Team sein eigenes Schema – und der Client sieht trotzdem nur einen Graphen?“

## Die Grundidee: Subgraphs, Supergraph, ein Endpoint

In Apollo Federation 2 – das ist die Generation, die seit 2022 relevant ist und die ich hier durchgehend meine – betreibt jedes Team einen **Subgraph**. Ein Subgraph ist ein ganz normaler GraphQL-Service mit eigenem Schema, eigener Datenbank, eigenem Deployment. Die einzelnen Subgraph-Schemas werden zu einem **Supergraph-Schema** komponiert. Zur Laufzeit sitzt davor ein Router (oder ein Gateway), der die eingehende Query entgegennimmt, einen Plan erstellt und die Teilfragen an die zuständigen Subgraphs verteilt.

Der entscheidende Punkt: Der Client merkt von all dem nichts. Er spricht mit einem Endpoint, sieht ein Schema, stellt eine Query. Die Aufteilung ist reine Betriebs- und Team-Topologie – für den Konsumenten unsichtbar.

```mermaid
flowchart TB
  Client["Client<br/>eine Query, ein Endpoint"]
  Router["Router / Gateway<br/>führt den Query-Plan aus"]
  Users["Users-Subgraph<br/>Team A"]
  Orders["Orders-Subgraph<br/>Team B"]
  UDB[("Users-DB")]
  ODB[("Orders-DB")]

  Client --> Router
  Router --> Users
  Router --> Orders
  Users --> UDB
  Orders --> ODB
```

Wer die Laufzeit betreibt, hat 2024 zwei etablierte Optionen: den **`@apollo/gateway`** in Node oder den in Rust geschriebenen **Apollo Router**. Beide sind gültig. Der Router ist der performantere Weg und die übliche Empfehlung für neue Setups, aber das Gateway ist keineswegs abgelöst – wer schon ein Node-Gateway betreibt, muss nicht umziehen.

## Rollen im System

Bevor Code entsteht, hilft es, die drei Bausteine sauber auseinanderzuhalten:

- Der **Subgraph** trägt genau seine eigenen Felder bei. Er kennt seine Datenbank, sein Schema, sein Deployment – und sonst nichts. Team-Autonomie steckt hier.
- Die Direktive **`@key`** markiert einen Typ als Entity, also als subgraph-übergreifend referenzierbar. Ohne `@key` kann ein anderer Subgraph den Typ weder erweitern noch per Referenz auf ihn zeigen.
- Der **Router** plant die Query. Er zerlegt eine eingehende Anfrage in Teilfragen, holt sich bei jedem Subgraph dessen Beitrag und fügt die Antwort wieder zusammen. Cross-Subgraph-Joins sind seine Aufgabe, nicht die der Teams.

Diese Trennung ist der ganze Trick: Teams denken lokal, der Router denkt global.

## Fed 2 ist explizit: das @link-Opt-in

Ein Subgraph in Federation 2 sagt ausdrücklich, welche Federation-Direktiven er nutzen will. Das geschieht über `@link` am Schema. Das ist ein bewusster Bruch mit Federation 1, wo `extend type ... @key` implizit ein bestimmtes Verhalten mitbrachte. In Fed 2 importiert man, was man braucht:

```graphql
extend schema
  @link(
    url: "https://specs.apollo.dev/federation/v2.0"
    import: ["@key", "@shareable", "@external"]
  )
```

Wer das `@link` vergisst oder eine Direktive nutzt, die nicht importiert wurde, bekommt keinen Laufzeitfehler, sondern einen Kompositionsfehler – und zwar früh, beim Komponieren oder Publizieren. Das überrascht Fed-1-Umsteiger regelmäßig, ist aber der bessere Zeitpunkt für einen Fehler.

## Der Users-Subgraph

Fangen wir mit dem einfachen Teil an. Der Users-Subgraph besitzt die `User`-Entity. Er definiert sie, markiert sie mit `@key` und liefert einen Reference-Resolver, der eine `User`-Repräsentation auflöst.

```graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}

type Query {
  user(id: ID!): User
}
```

Der Code dazu nutzt `buildSubgraphSchema` aus dem Paket `@apollo/subgraph`. Das ist der Baustein, der aus `typeDefs` und `resolvers` ein federiertes `GraphQLSchema` macht, das die Federation-Direktiven und die von der Spec verlangten Felder wie `_entities` kennt.

```ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    user(id: ID!): User
  }
`;

const resolvers = {
  Query: {
    user: (_parent, { id }) => fetchUserById(id),
  },
  User: {
    // The router hands us a representation: { __typename: "User", id }
    __resolveReference: (reference) => fetchUserById(reference.id),
  },
};

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers }),
});

const { url } = await startStandaloneServer(server, { listen: { port: 4001 } });
console.log(`Users subgraph ready at ${url}`);
```

Das Herzstück ist `__resolveReference`. Diese Funktion bekommt eine **Representation** – ein Objekt mit `__typename` und den `@key`-Feldern – und liefert die vollständige Entity zurück (oder `null`, wenn es sie nicht gibt). Wichtig zu verstehen: Ein normaler Feld-Resolver reicht hier nicht, weil der Router den Auflösungsprozess nur mit den Key-Feldern startet. Ohne Reference-Resolver kann er die Lücke nicht füllen.

## Der Orders-Subgraph mit Cross-Reference

Jetzt wird es interessant. Der Orders-Subgraph besitzt `Order`. Eine Order gehört einem User – aber der Orders-Subgraph kennt von `User` nur die ID. Er will trotzdem ein Feld `order.user` anbieten. Dafür nimmt er `User` als Stub auf: mit `@key`, aber nur mit dem Key-Feld, ohne eigene Auflösung der übrigen Felder.

```graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

type Order @key(fields: "id") {
  id: ID!
  total: Int!
  user: User
}

# User lives in the users subgraph; here it is only referenced.
type User @key(fields: "id") {
  id: ID!
}

type Query {
  order(id: ID!): Order
}
```

Der Resolver für `Order.user` macht etwas, das anfangs fast zu simpel wirkt: Er gibt nur eine Referenz zurück – `__typename` plus Key-Feld. Die eigentliche Auflösung von `name` oder `email` passiert später im Users-Subgraph.

```ts
const resolvers = {
  Query: {
    order: (_parent, { id }) => fetchOrderById(id),
  },
  Order: {
    __resolveReference: (reference) => fetchOrderById(reference.id),
    // Return just a reference; the users subgraph fills in the rest.
    user: (order) => ({ __typename: 'User', id: order.userId }),
  },
};
```

So bleibt jede Verantwortung dort, wo sie hingehört. Das Orders-Team weiß, welcher User zu einer Order gehört (die `userId` liegt in seiner DB), aber es muss die User-Daten nicht selbst kennen. Der Router übernimmt den Rest.

## Was der Router im Hintergrund tut

Nehmen wir eine Client-Query, die beides überspannt:

```graphql
query {
  order(id: "42") {
    total
    user {
      name
    }
  }
}
```

Der Client sieht eine Query. Der Router sieht einen Plan. Er holt zuerst `total` und die `user.id` aus dem Orders-Subgraph, schickt dann eine `_entities`-Anfrage mit der Representation `{ __typename: "User", id }` an den Users-Subgraph, dessen `__resolveReference` daraus den `name` besorgt, und merged am Ende alles zu einer Antwort.

```mermaid
sequenceDiagram
  participant C as Client
  participant R as Router
  participant O as Orders-Subgraph
  participant U as Users-Subgraph

  C->>R: order(id: 42) { total, user { name } }
  R->>O: order { total, user { id } }
  O-->>R: { total: 90, user: { id: 7 } }
  R->>U: _entities([{ __typename: User, id: 7 }])
  U-->>R: { name: "Ada" }
  R-->>C: { total: 90, user: { name: "Ada" } }
```

Wenn dieser Ablauf einmal sitzt, macht es „klick“: `@key` sagt, woran eine Entity erkennbar ist, `__resolveReference` sagt, wie man sie aus dieser Erkennung vollständig macht, und der Router orchestriert dazwischen.

## Komponieren und starten

Die Subgraph-Schemas müssen zu einem Supergraph zusammengeführt werden. Das ist ein Build- oder Publikationsschritt, keine Aufgabe des Routers während einer Anfrage. Lokal geht die Komposition mit `rover supergraph compose` und einer Konfiguration, die auf die Subgraphs zeigt:

```bash
# supergraph.yaml lists each subgraph and its endpoint
rover supergraph compose --config ./supergraph.yaml > supergraph.graphql

# Start the Apollo Router with the composed supergraph
./router --supergraph supergraph.graphql
```

In größeren Setups übernimmt Managed Federation über Apollo Studio / GraphOS diesen Schritt: Jeder Subgraph publiziert sein Schema, die Komposition passiert zentral, der Router zieht die aktuelle Supergraph-Version. Der wichtige Punkt bleibt derselbe – die Komposition ist der Moment, in dem Fehler auffallen, nicht die Laufzeit.

## Geteilte Felder: der Unterschied zu Federation 1

Ein Detail, an dem Fed-1-Umsteiger regelmäßig stolpern: In Federation 2 sind Nicht-Key-Felder standardmäßig **nicht** teilbar. Wenn dasselbe Feld eines Typs in mehreren Subgraphs aufgelöst wird, muss es explizit mit `@shareable` markiert sein – sonst schlägt die Komposition fehl. Das betrifft vor allem Value-Types, die in mehreren Subgraphs identisch vorkommen.

```graphql
type Money @shareable {
  amount: Int!
  currency: String!
}
```

Verwandt ist `@external`: Ein Feld, das in diesem Subgraph nur referenziert wird und einem anderen gehört, wird als `@external` markiert – häufig zusammen mit `@requires` oder `@provides`, wenn ein Subgraph für seine Logik ein fremdes Feld braucht. Fed 2 ist an dieser Stelle strenger als Fed 1, und das ist Absicht: Mehrdeutigkeiten werden zum Fehler, statt sich still in der Laufzeit zu verstecken.

## Drei Fallstricke

Federation löst ein Organisationsproblem, handelt sich dafür aber technische Eigenheiten ein. Drei davon fallen besonders ins Gewicht.

Im Reference-Resolver lauert N+1. Der Router löst Entities einzeln oder in Batches auf, und wenn `__resolveReference` naiv pro Aufruf einen DB-Roundtrip macht, entstehen viele kleine Anfragen. Ein DataLoader pro Subgraph-Request bündelt sie – dasselbe Muster, das man auch außerhalb von Federation gegen N+1 einsetzt.

Das Key-Feld muss außerdem verlässlich in der Payload landen. Ein Feld mit Argumenten oder ein Union- bzw. Interface-Rückgabetyp taugt nicht als `@key`; ein nullbares Feld ist erlaubt, aber nicht empfehlenswert. Unterschiedliche Key-Felder für dieselbe Entity über verschiedene Subgraphs sind zwar zulässig, aber fehleranfällig – im Zweifel überall denselben Key nutzen.

Und der dritte Punkt trifft jeden, der aus einer Welt kommt, in der solche Dinge erst im Betrieb knallen: Ein vergessenes `@shareable`, ein Typ-Mismatch, ein fehlender `@link`-Import – all das wird schon beim Komponieren oder Publizieren abgelehnt, nicht zur Laufzeit. Der richtige Zeitpunkt, aber ein ungewohnter.

## Federation 2 oder Schema-Stitching?

Wer länger mit GraphQL arbeitet, kennt noch **Schema-Stitching** – den älteren Weg, mehrere Schemas zusammenzuführen, heute etwa mit `@graphql-tools/stitch`. Beim Stitching kombiniert man Remote-Schemas manuell und schreibt Delegations- und Merge-Logik selbst. Das funktioniert, aber die Kopplung liegt im Gateway-Code, nicht in einer Spec. Federation deklariert die Beziehungen stattdessen im Schema (`@key`, `@shareable`) und überlässt dem Router die Ausführung. Stitching ist nicht tot – für manche Sonderfälle mit fremden, nicht anpassbaren Schemas ist es weiterhin brauchbar. Für eine neue, teamgetriebene Architektur greife ich 2024 aber zu Federation 2.

## Wann ich NICHT federiere

Der wichtigste Satz zum Schluss, weil er in Architektur-Diskussionen am meisten Widerspruch auslöst: Federation ist Overhead. Router, Komposition, Reference-Resolver, ein zusätzlicher Netzwerk-Hop pro Cross-Subgraph-Join – das alles will betrieben und verstanden sein. Wenn ein einzelnes Team ein Schema betreibt, das gut in einen Service passt, dann löst Federation ein Problem, das dieses Team nicht hat. Man handelt sich verteilte Systeme ein, um eine Organisationsgrenze abzubilden, die noch gar nicht existiert.

Federation folgt der Team-Struktur, nicht der Technik. Erst wenn mehrere Teams sich an einem Schema in die Quere kommen, wenn Deployments sich koppeln, wenn ein `type` niemandem mehr gehört – dann zahlt sich der Schnitt aus. Vorher ist ein guter Monolith mit klaren Modulen fast immer die bessere Wahl. Und wer clientseitig ohnehin schon sauber mit [Apollo-Client-Caching](https://www.mikebild.dev/de/blog/graphql-client-caching-mit-apollo/) arbeitet, merkt: An der Client-Seite ändert Federation nichts – dort bleibt es genau der eine Graph, der es immer war.

## Weiterführende Quellen

- Introduction to Apollo Federation: https://www.apollographql.com/docs/federation/
- Entities (`@key`, `__resolveReference`): https://www.apollographql.com/docs/federation/entities
- Implementing a Subgraph with Apollo Server: https://www.apollographql.com/docs/federation/subgraphs/
- API `@apollo/subgraph` (`buildSubgraphSchema`): https://www.apollographql.com/docs/apollo-server/using-federation/api/apollo-subgraph
- Moving to Federation 2 (inkl. `@shareable`): https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/reference/moving-to-federation-2
- Subgraph-Spec (`_entities`, `_Entity`, `@link`): https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/reference/subgraph-spec
