skip to content

What do the join__ directives inside a composed federation supergraph schema record?

level: middleimportance: should knowfreq 41%

answer

  1. Metadata a planner needs, not a client
  2. One enum value per service
  3. Repeated once per declaring subgraph
  4. Field-level annotation names the owner
  5. The @link stamps the version

basics

~20 s

They record the routing metadata composition adds: a generated join__Graph enum lists each subgraph with its URL, @join__type says which subgraphs declare a type and with what key, and @join__field says which subgraph resolves each field.

solid answer

~50 s

They are the routing metadata a query planner needs, written into the composed document as ordinary SDL directives. An `@link` on the schema definition declares which specifications the document uses and at what version, so a reader knows the directive set. A generated `join__Graph` enum has one value per subgraph, each carrying that subgraph's name and URL. `@join__type` is repeated on a type once per subgraph that declared it, carrying that subgraph's key when it is an entity. `@join__field` names the subgraph that resolves a field and carries its federation semantics — the required sibling fields, what it provides, whether it is merely external, and where ownership was overridden from. Together that is enough to plan any operation without contacting a subgraph. None of it is defined by the GraphQL specification; it is Apollo Federation's composition output.

code

graphql · 26 lines
graphql
schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
  query: Query
}

scalar join__FieldSet

enum join__Graph {
  BREEDING @join__graph(name: "breeding", url: "http://breeding.internal/graphql")
  REGISTRY @join__graph(name: "registry", url: "http://registry.internal/graphql")
  SHOWS    @join__graph(name: "shows",    url: "http://shows.internal/graphql")
}

type Animal
  @join__type(graph: BREEDING, key: "id")
  @join__type(graph: REGISTRY, key: "id")
  @join__type(graph: SHOWS, key: "id")
{
  id: ID!
  name: String! @join__field(graph: REGISTRY)
  birthDate: Date! @join__field(graph: REGISTRY)
  sire: Animal @join__field(graph: BREEDING)
  showEligible: Boolean! @join__field(graph: SHOWS, requires: "birthDate")
}

go deeper

for a junior

You are not expected to read a supergraph document yet. Know that composition writes extra directives into it recording which service owns which field, and that clients never see them.

for a middle

Be able to walk a short supergraph excerpt out loud: the enum of subgraphs and their URLs, the per-subgraph type annotations with their keys, and the field annotation naming the resolving subgraph. This is the level the question is usually asked at.

for a senior

Show you use the document diagnostically — reading it to find which service actually backs a field, and knowing that a requires annotation on a field is a visible ordering constraint on the plan before anything runs.

for a principal

Own the versioning consequence: the directive set is stamped by a specification URL, so routers, composition and any in-house tooling that parses supergraphs form a compatibility set you have to upgrade deliberately.

## What composition has to write down A federation router receives an operation written against the API schema and must turn it into a plan: call these services, in this order, sending these key fields. To do that it needs facts the API schema deliberately does not contain — which service can produce which field, how to address an entity in each service, and where each service actually lives. Composition records those facts as directives in the supergraph document, under a `join__` prefix. Continue with the livestock pedigree graph: a **registry** subgraph owning an animal's identity, a **breeding** subgraph owning lineage, a **shows** subgraph owning competition entries. ## The pieces **`@link` on the schema definition.** The supergraph opens by declaring which specifications it uses and at what version — the link spec itself, and the join spec that defines every `join__` directive below. Anything reading the document reads this first, because the directive set is versioned: what a supergraph looks like is a function of the join version stamped here, not a fixed shape. **The `join__Graph` enum.** Composition generates one enum value per subgraph, each carrying `@join__graph(name:, url:)`. The enum values are the identifiers every other join directive uses to refer to a subgraph, and the URLs are why a router needs no service discovery pass at startup and no runtime introspection of its subgraphs. **`@join__type`.** Repeatable, once per subgraph that declared the type. On an entity it carries that subgraph's `key` as a field-set string. Reading the annotations on `Animal` tells a planner: registry has it with key `id`, breeding has it with key `id`, shows has it with key `id` — so any of the three can be entered given an `id`. **`@join__field`.** Says which subgraph resolves a particular field, and carries the field's federation semantics with it — the `requires` field set when the owning subgraph needs sibling fields fetched first, `provides` when it can supply a neighbour's field inline, `external` when it merely references a field it does not own, and the source subgraph when ownership was moved with an override. **Supporting declarations.** The document also carries the directive definitions themselves and a `join__FieldSet` scalar, so that the supergraph is a valid, self-describing GraphQL document rather than one that only a particular tool can parse. ## Reading one ```graphql type Animal @join__type(graph: BREEDING, key: "id") @join__type(graph: REGISTRY, key: "id") @join__type(graph: SHOWS, key: "id") { id: ID! name: String! @join__field(graph: REGISTRY) birthDate: Date! @join__field(graph: REGISTRY) sire: Animal @join__field(graph: BREEDING) dam: Animal @join__field(graph: BREEDING) showEligible: Boolean! @join__field(graph: SHOWS, requires: "birthDate") } ``` Everything a planner needs is on the page. A document selecting `name` and `sire` needs two services. A document selecting `showEligible` needs the shows subgraph, but only after `birthDate` has been fetched from registry and passed back in the representation — the `requires` argument is what makes that ordering constraint visible without asking any service anything. Note what `id` does **not** carry: no `@join__field` at all. A field with no annotation is resolvable from every subgraph that declares the type, so the planner may take it from whichever subgraph it is already visiting rather than adding a hop. Key fields are the usual case; a value type declared identically everywhere is the other. ## The two things people get wrong **These are not GraphQL.** No `join__` directive appears in the GraphQL specification. They are Apollo Federation's composition output — a convention layered on ordinary SDL, which is precisely why an ordinary GraphQL parser can read the document without understanding a word of it. Saying "the spec defines join__type" in an interview is a tell. **They are not stable across versions.** The join specification has moved: earlier editions expressed ownership differently, including a separate owner directive that later versions dropped in favour of putting the graph on each field. Any tool that reads supergraph documents must branch on the version in the `@link`, and a supergraph composed by one version is not guaranteed to be consumable by a router built for another. ## Why you might ever read one by hand You should not edit a supergraph — it is a build artefact, and the next composition overwrites it. But reading one is the fastest answer to "which service is actually behind this field?", which is the first question worth asking when one field in a pedigree query is slow, or when a field a team swears they own turns out to be annotated with a different graph.

  • Why does a federation router not need to introspect its subgraphs at runtime?
    Because the supergraph already contains everything a plan needs. The `join__Graph` enum carries each subgraph's URL, `@join__type` carries its keys, and `@join__field` says which subgraph resolves what. Subgraph schemas are read once, at composition time. A router that had to introspect per request would add a round trip to every operation and would be planning against a schema that could differ from the one composition validated.
  • A field in a supergraph carries no @join__field at all. What does that mean?
    That it is resolvable from every subgraph that declares the type, so the planner is free to take it from whichever subgraph it is already visiting rather than adding a fetch. Key fields are the common case, since every subgraph declaring an entity must be able to produce its key; a value type declared identically in several subgraphs is the other.
  • Should anyone ever edit a supergraph document by hand?
    No. It is a build artefact and the next composition run overwrites it, so a hand edit is lost at best and a schema the subgraphs cannot actually satisfy at worst. Reading one is fair game and often the fastest way to answer which service is behind a slow field, but changes belong in the subgraph SDL that composition reads.

saying these in an interview costs you the question

  • Thinks join__ directives come from the GraphQL specification
  • Says the router introspects subgraphs on each request
  • Confuses join__Graph enum values with GraphQL type names
  • Expects clients to see join__ directives in introspection
  • Assumes every field carries a @join__field
  • Edits the composed supergraph document by hand

context