Skip to content

Federation Patterns

How to design schemas that work well with Apollo Federation, including entity thinking, entity keys, directives, cross-subgraph extension, governance, and versioning.

Based on Apollo's Schema Design and Thinking in Entities guides.

Thinking in Entities

Apollo's core design principle: entities are the building blocks of a federated graph. An entity is any type that can be referenced and extended across subgraphs.

  1. Does this type have a unique identity? If it has an id and a lifecycle, it is likely an entity.
  2. Will multiple subgraphs need to reference or extend it? If yes, it must be an entity.
  3. Does it represent a core domain concept? Match, User, Team are entities. Address, Score, PageInfo are not.

Entities vs. Value Types

EntityValue Type
Has @keyYesNo
Has identityYes (id)No (defined by its fields)
Referenced across subgraphsYesNo
ExampleMatch, User, TeamAddress, Venue, PageInfo, Money

Value types are plain types embedded within entities. They do not need @key because they are not independently resolvable -- they always come as part of an entity.

graphql
# Entity -- has identity, referenced across subgraphs
type Match @key(fields: "id") {
	id: ID!
	venue: Venue # value type, embedded
}

# Value type -- no identity, no @key
type Venue {
	name: String!
	city: String!
}

Entity Keys

Mark types that can be referenced across subgraphs with @key. Use the minimal identifying fields, almost always just id.

graphql
type Match @key(fields: "id") {
	id: ID!
	homeTeam: Team!
	awayTeam: Team!
}

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

Multiple keys are allowed when an entity can be looked up by different fields:

graphql
type Order @key(fields: "id") @key(fields: "orderNumber") {
	id: ID!
	orderNumber: String!
}

Entity Resolution

Every entity must implement __resolveReference so the router can fetch it by key:

typescript
Match: {
  __resolveReference: (ref: { id: string }) => {
    return getMatchById(ref.id);
  },
}

Extending Entities Across Subgraphs

A subgraph can add fields to an entity it does not own:

graphql
# Sports subgraph owns Match
type Match @key(fields: "id") {
	id: ID!
	homeTeam: Team!
	awayTeam: Team!
}

# POTM subgraph extends Match
extend type Match @key(fields: "id") {
	id: ID! @external
	playerOfTheMatch: PlayerCandidate
}

The router stitches both fragments together.

Directives Reference

DirectivePurpose
@key(fields: "...")Declares an entity and its lookup key
@externalMarks a field defined in another subgraph (needed by @requires / @provides)
@requires(fields: "...")This field needs data from another subgraph to resolve
@provides(fields: "...")This subgraph can resolve extra fields on a returned entity
@shareableMultiple subgraphs can resolve this field (use sparingly)

When to Use @requires

Only when a field's resolver genuinely needs a value owned by another subgraph. It adds a dependency between subgraphs, so prefer fetching the data client-side or restructuring the schema to avoid it.

Query Namespacing

Do not namespace queries. All queries must be defined directly on the root Query type.

Apollo documents a type-as-namespace pattern where root queries are grouped under synthetic domain types:

graphql
# Do not use this pattern
type Query {
	sports: SportsQueries!
	business: BusinessQueries!
}

type SportsQueries {
	match(id: ID!): Match
	matches(first: Int, after: String): MatchConnection!
}

This pattern has a critical performance problem in federation. The router resolves the namespace field (sports) as a fetch to a subgraph before it can resolve any of the nested fields. This adds an extra round-trip to every query; the router cannot plan the actual data fetches until the namespace resolver returns. With flat root fields, the router can plan and dispatch all fetches in a single pass.

Beyond performance, namespace types are synthetic types with no domain meaning. Any subgraph that wants to contribute fields under the same namespace must own or extend that type, creating cross-subgraph coupling. Flat root fields let each subgraph extend Query independently with no coordination required.

Use flat, field names instead:

graphql
extend type Query {
	match(id: ID!): Match
	matches(first: Int, after: String): MatchConnection!
	standings(competitionId: ID!): [Standing!]!
}

Schema Governance

Apollo recommends treating schema changes as a controlled process, especially in federation where a change in one subgraph can break the composed supergraph.

Composition Checks in CI

Run rover supergraph compose in CI on every PR that touches a schema.graphql file. If composition fails, the PR cannot merge. This catches type conflicts, missing entity references, and directive errors before deployment.

Schema Change Review

Treat schema changes like API contract changes:

  • Additive changes (new fields, new types): safe, no review needed beyond normal code review.
  • Deprecations: require a reason and a removal timeline in the @deprecated directive.
  • Removals: require verification that no client is still using the field (check query analytics if available).
  • Breaking changes (renaming fields, changing types): should go through deprecate-then-remove cycle.

Versioning

GraphQL APIs should not use URL versioning (/v1/graphql). Instead:

  1. Add fields freely: additive changes never break clients.
  2. Deprecate before removing: use @deprecated(reason: "...") with a timeline.
  3. Never remove without notice

If we later need parallel API versions, consider the model: dated versions (e.g. 2025-04) with automatic deprecation schedules. For now, field-level deprecation is sufficient, especially since we need to consider old React Native clients.