skip to content

In a federated subgraph schema, what does @key(fields: "id") on a type declare?

level: juniorimportance: must knowfreq 62%

answer

  1. Federation, not the GraphQL specification
  2. Marks a type as shared across services
  3. The argument is a selection set
  4. An identity another service can point at
  5. Also a promise this subgraph can look it up

basics

~20 s

It marks the type as an entity: an object other subgraphs may reference. The fields argument is the selection set that identifies one instance, and the subgraph declaring it promises it can look an instance up from those fields alone.

solid answer

~50 s

`@key` comes from Apollo Federation's composition specification, not from the GraphQL specification, which has no notion of object identity at all. Putting `@key(fields: "id")` on `type Account` says three things at once: `Account` is an **entity**, so several subgraphs may contribute fields to it; `id` identifies exactly one `Account`; and *this* subgraph can produce an `Account` given nothing but its `id`. Another service in a bank statements graph can then declare the same type with the same key and add its own fields — `Account @key(fields: "id") { statements(period: String!): [Statement!]! }` — without knowing anything else about accounts. Composition merges the two declarations into one `Account` in the supergraph and records which subgraph supplies what, so the key becomes the join column of a distributed graph. Types with no `@key` are ordinary value types and cannot be joined this way.

code

graphql · 18 lines
graphql
# accounts subgraph
type Account @key(fields: "id") {
  id: ID!
  displayName: String!
  openedOn: Date!
}

# statements subgraph
type Account @key(fields: "id") {
  id: ID!
  statements(period: String!): [Statement!]!
}

type Statement {
  id: ID!
  period: String!
  closingBalanceMinor: Int!
}

go deeper

for a junior

Be ready to say in one sentence that @key marks a type as an entity and names the fields that identify one instance, and to write the same @key on the same type in two subgraph schemas.

for a middle

Explain the three claims the directive makes at once — entity, identity, and this subgraph's ability to look one up — and that the key fields must be resolvable in the subgraph declaring them.

for a senior

An interviewer expects you to talk about key stability and choice: a key is the object's name in the graph for its whole lifetime, and picking a mutable or reused value is a defect you pay for later.

for a principal

Own the boundary decision: which types in the graph are genuinely referable entities versus value types, and what it costs the organisation when every team declares keys on everything and the join surface becomes unbounded.

## GraphQL has no idea what an object *is* The GraphQL specification describes a type system and an execution algorithm. It is silent on object identity. If one response contains an `Account` with `id: "acct-8823041"` and a second response contains another, nothing in the specification says they are the same account — nothing in it can even ask the question. Inside a single server that never matters, because one schema and one set of resolvers own every path a client can walk. It starts to matter the moment a client-facing graph is assembled out of several independently deployed services. In an 11-service bank statements graph, the service that owns account records is not the service that owns statement lines, and neither owns the payment-instruction history. Something has to state that the `Account` the first service returns and the `Account` the second service can extend are **the same object**. `@key` is that statement. It is defined by Apollo Federation's composition specification — a layer on top of GraphQL, not part of it. ## Entities and value types A type carrying at least one `@key` is an **entity**. Entities can be split across subgraphs and rejoined by identity. Every other type is a value type: it exists only inside whatever response produced it, and two subgraphs that each define a `Money` type are not claiming to describe the same `Money`. That is the first design decision the directive forces: which of your types are things the graph refers to, and which are just shapes. ## The three promises in one directive Writing this in the accounts subgraph: ```graphql type Account @key(fields: "id") { id: ID! displayName: String! openedOn: Date! } ``` makes three separate claims. 1. **`Account` is an entity.** Other subgraphs are allowed to declare a type with this name and contribute fields to it. 2. **`id` identifies exactly one `Account`.** This is an identity contract, and it is taken entirely on faith — nothing in composition or at runtime checks that the values are unique. 3. **This subgraph can produce an `Account` given only `{ id }`.** A capability promise: if the router hands this service an identity, it can hand back the object. The fields named in a key must be resolvable *in the subgraph that declares the key*. You cannot key on a field only some other service knows how to compute. ## What the referencing subgraph writes The statements subgraph writes almost nothing about accounts: ```graphql type Account @key(fields: "id") { id: ID! statements(period: String!): [Statement!]! } ``` Same type name, same key, its own field. It does not know the display name, the opening date, or where account records are stored. That independence is the entire point: the two teams share one line of schema — the key — and nothing else. ## What composition does with it Composition merges every subgraph's declaration of `Account` into a single type in the supergraph, recording for each field which subgraph can supply it and for each subgraph which keys it accepts. At request time a document asking for `account(id: ...) { displayName statements(period: "2026-07") { ... } }` becomes a plan: fetch from the accounts subgraph, carry the key fields forward, then ask the statements subgraph for the rest using only those key fields. In relational terms the key is the join column; in practice it is the only thing that crosses the service boundary. ## Details that trip people up **The field need not be called `id`, and need not be of type `ID`.** `@key(fields: "iban")` is perfectly legal if `iban` identifies an account and the subgraph can look one up by it. The `id` convention is borrowed from habit, not required by the directive. **`@key` is repeatable.** A type may carry several keys — `@key(fields: "id") @key(fields: "sortCode accountNumber")` — and the router uses whichever one it can satisfy from data it already holds. That is how a subgraph that only ever learns an account's sort code and number can still point at the same entity, and it is also the mechanism that makes changing a key survivable. **A key must be stable for the object's lifetime.** It is not a lookup convenience, it is the object's name in the graph. If a branch merge re-issued account identifiers, every reference held elsewhere would silently point at a different account or at nothing. **Clients never touch it.** The key is a server-side composition artefact. A client queries `Account` the way it would in any single-service schema; it does not send a key, and federation directives are not something a client-facing schema normally exposes.

  • Must the key field be called `id` or be of type `ID`?
    No. `@key(fields: "iban")` is legal as long as an IBAN identifies exactly one account and the declaring subgraph can look an account up by it. The `id: ID!` habit is convention. What the directive actually requires is that the named fields exist in this subgraph, are resolvable here, and together pick out one object.
  • Can one type carry more than one @key?
    Yes — `@key` is repeatable. A type may declare `@key(fields: "id")` and `@key(fields: "sortCode accountNumber")`, and the router uses whichever key it can satisfy from the data it has already fetched. Alternative keys let a subgraph that only ever learns one identifier still reference the entity, and they are the mechanism for migrating a key without a coordinated deploy.
  • What happens to a type that has no @key at all?
    It is a value type. It can appear in a subgraph's schema and be returned inside that subgraph's responses, but nothing can point at a specific instance of it from elsewhere, and no other subgraph can add fields to a particular one. Only entities are joinable across services.

A key is the account number printed on a bank statement, not the statement itself: two departments can each hold a different piece of paper about you, and the number is the only thing they need to agree on to know it is the same customer.

saying these in an interview costs you the question

  • Says @key is part of the GraphQL specification
  • Thinks the key field must be named id
  • Believes @key creates a database index or constraint
  • Thinks clients pass the key with their queries
  • Assumes any type shared by two subgraphs is an entity
  • Treats the key as a lookup hint rather than an identity

context