Subgraph Implementation & Entity Resolution: Architectural Patterns & Distributed Workflows

Entity resolution is the mechanism that lets an Apollo Federation v2 supergraph behave like a single schema while every type is owned, deployed, and scaled by an independent service. This section is the engineering reference for that mechanism: how subgraphs declare identity with @key, how the router stitches partial representations into whole objects, and how teams keep those contracts correct as services evolve on independent release cadences.

The transition from a monolithic GraphQL schema to a federated graph is not a schema split — it is a shift toward domain-driven service boundaries and explicit data contracts. Platform teams must treat each subgraph as an independent product with its own ownership, versioning, and deployment lifecycle, while still guaranteeing that the composed supergraph resolves cross-service queries deterministically. The pages in this section cover every layer of that problem, from the foundational @key contract through field-level dependencies, custom scalars, shared enums, authorization directives, and reference-resolver performance. For the architectural decisions that sit above implementation — where boundaries fall, how types are owned, how schemas compose — see GraphQL Federation Architecture & Design. For everything that happens after a correct schema ships — router deployment, tracing, caching, and migrations — see Federated GraphQL Operations in Production.

Core Concepts Overview

Federated entity resolution rests on a small set of directives and one runtime contract. Each guide in this section takes one concern to production depth. Read them in roughly this order when you are building a subgraph from scratch.

  • Identity and reference hydration. The @key directive declares which fields uniquely identify an entity, and a __resolveReference resolver hydrates a partial representation into a full object. Start with Implementing Entity Resolvers with @key Directives, which establishes the foundational contract for how the router identifies, fetches, and merges partial entity representations across service boundaries. When a key field is dropped or mistyped, composition fails or the router silently nulls a branch — diagnosed in Debugging Missing @key Fields in Apollo Federation v2.

  • Cross-service field dependencies. When one subgraph needs a field owned by another to compute its own data, it declares that dependency explicitly. Using @external and @requires for Field Resolution shows how to declare cross-service data requirements without violating encapsulation, with @provides and @requires chains modelled as real edges in the query plan.

  • Reference resolver performance. Every cross-subgraph hop is a potential N+1. Optimizing Reference Resolvers for Performance covers DataLoader batching, projection filtering, and cache-aware fetching to keep the supergraph fast under concurrent load.

  • Domain primitives. Distributed schemas drift when shared types diverge. Custom Scalars in Federated GraphQL Schemas addresses serialisation consistency for domain-specific primitives like currency codes or geospatial coordinates, and Managing Shared Enums Across Subgraphs gives strategies for centralised enum governance so independent deployments do not break composition.

  • Decentralised authorization. Security in a federated graph belongs at the field, enforced locally. Directive Patterns for Cross-Service Authorization outlines schema-layer access control with @authenticated, @requiresScopes, and @policy, so teams enforce consistent rules without centralising policy logic.

Effective federation begins with strict domain isolation. Each subgraph owns a discrete slice of the business domain; overlapping ownership creates composition conflicts and unpredictable query plans. The router relies on explicit entity contracts — and nothing else — to route field resolution correctly, so the quality of those contracts determines the reliability of the whole graph.

There is an order to these concerns, and it is worth internalising before writing any SDL. Identity comes first: until the router can name an entity unambiguously, nothing else can be resolved across a boundary. Once identity is stable, field dependencies decide what data has to travel with a representation and in what sequence. Performance is a property of how those dependencies are fetched — batched or not, projected or not, cached or not. Type governance keeps the contracts consistent as services ship independently, and authorization decides which parts of a resolved entity a given caller may actually see. Skipping a layer does not remove its cost; it defers the cost to an incident. A graph with brilliant batching but an unstable @key will be fast and wrong. A graph with airtight authorization but drifting enums will reject valid traffic during deploys. The pages in this section are sequenced to match that dependency order, and the rest of this overview reflects it.

A second mental model worth carrying through every page: a federated entity is not stored anywhere whole. It exists only as the merge of contributions from each subgraph that declares it, assembled per request by the router. No single service can answer “what is this User” completely, and no database row corresponds to the composed type. That is the source of both federation’s power — independent teams extend shared entities without coordinating deploys — and its failure modes — a representation that loses a key field, a contributed branch that nulls out, an enum value one subgraph has not learned about yet. Designing subgraphs well means designing for that assembled, partial, per-request reality rather than the monolithic object it resembles in the client.

Architecture Diagram

The diagram below shows the path of a single client operation through a federated graph. The router never queries a data source directly; it builds a query plan from the composed supergraph SDL, fetches base representations from owning subgraphs, packages @key fields into entity representations, and dispatches _entities batch fetches to hydrate the rest.

Federated entity resolution flow A client sends one operation to the Apollo Router. The router fetches a base User from the accounts subgraph, extracts the id key field into a representation, dispatches an _entities fetch to the orders subgraph whose __resolveReference hydrates the entity, and merges both into one response. Client one operation Apollo Router build query plan fetch · represent merge accounts subgraph owns User @key(id) returns base entity orders subgraph extends User @key(id) __resolveReference hydrates orderTotal query 1. fetch User 2. _entities({id}) 3. merged response → client

Walking the plan: the client sends one operation to the router. The router consults the composed supergraph and produces an execution plan — a directed acyclic graph of fetch steps. It fetches the base User from the accounts subgraph that owns the type. From that response it extracts the @key fields, packages them into an entity representation ({ __typename: "User", id: "..." }), and dispatches a batched _entities query to the orders subgraph. That subgraph’s __resolveReference resolver receives the representation, loads the full record, and returns the locally owned fields. The router merges every branch by shared key and returns one response. The router executes parallel fetches wherever the dependency graph allows; misaligned keys or @requires chains that depend on each other force sequential fetches and degrade latency.

Nullability propagation follows strict GraphQL rules. A non-nullable field that fails resolution collapses its parent object, so design SDL to reflect realistic failure boundaries: use nullable fields for cross-service dependencies that may experience transient outages, and the router will return null for that branch while preserving successfully resolved data.

The shape of the plan is worth dwelling on, because almost every latency problem in a federated graph is a plan problem. The router does not execute the client’s selection set directly; it rewrites it into the smallest set of subgraph fetches that can satisfy it, ordered by dependency. Two fetches that do not depend on each other run in parallel; a fetch that needs the output of another waits. Three things make the plan longer and slower than it should be. First, a @requires chain that forces field B to wait for field A even though both could have been fetched together. Second, a key whose fields are scattered across the response so the router must make an extra fetch just to assemble the representation. Third, an entity that could have been resolved with a @provides hint in one hop but instead triggers a separate _entities round trip. Reading a query plan — which the router will print on request — is the single most useful debugging skill for federated performance, and it is the lens through which the reference-resolver and field-dependency guides in this section should be read.

Failure isolation is the other half of the runtime story. Because the router merges branches independently, a federated graph degrades gracefully only if the schema lets it. A nullable cross-service field lets the router drop one branch and return the rest; a non-null field on the same dependency turns a transient subgraph timeout into a collapsed parent object and, if that object is itself non-null in a list, a collapsed list. The nullability of cross-service fields is therefore a reliability decision, not a modelling nicety. Pair conservative nullability with circuit breakers around external calls and explicit fallback resolvers, and a single slow subgraph becomes a missing field rather than a failed operation.

Key Directives & Config Reference

These are the directives this section uses most. Each is imported through the federation @link on a per-subgraph basis; import only what a subgraph actually uses.

Directive Purpose Resolved at
@key(fields: "...") Declares the identity fields the router uses to build entity representations Composition + runtime
@external Marks a field as owned by another subgraph, referenced locally for planning Composition
@requires(fields: "...") Forces the router to fetch named external fields before the local resolver runs Runtime (query plan)
@provides(fields: "...") Declares that a field can supply specific external fields, avoiding an extra hop Query plan
@shareable Allows a non-key field to be resolved by more than one subgraph Composition
@override(from: "...") Migrates ownership of a field from one subgraph to another Composition
@inaccessible Hides a field from the public supergraph while keeping it for composition Composition
@authenticated Restricts a field to authenticated requests Runtime
@requiresScopes(scopes: [...]) Restricts a field to requests carrying the listed scopes Runtime
@policy(policies: [...]) Delegates an authorization decision to a named policy evaluated by the router Runtime

A minimal Apollo Router configuration that supports entity resolution with query-plan caching looks like this:

# router.yaml (Apollo Router)
supergraph:
  listen: 0.0.0.0:4000
  introspection: false
query_planning:
  cache:
    in_memory:
      limit: 512          # cached query plans, keyed by operation
telemetry:
  exporters:
    logging:
      stdout:
        enabled: true
        format: json

Canonical Implementation Pattern

Every entity-owning subgraph follows the same shape: declare the type with @key, build the schema with buildSubgraphSchema, and implement __resolveReference to hydrate a representation. The owning subgraph below owns User and resolves it from a data source.

# accounts subgraph SDL
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@shareable"])

type User @key(fields: "id") {
  id: ID!
  email: String!
  displayName: String
}
// accounts subgraph server
import { buildSubgraphSchema } from "@apollo/subgraph";
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { gql } from "graphql-tag";
import { getUserById } from "./db";

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

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

const resolvers = {
  User: {
    // reference arrives as { __typename: "User", id: "..." }
    __resolveReference: async (reference: { id: string }) => {
      const user = await getUserById(reference.id);
      if (!user) {
        // throw, never silently return null — a silent null drops the branch
        throw new Error(`User entity not found for id: ${reference.id}`);
      }
      return { __typename: "User", ...user };
    },
  },
};

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

const { url } = await startStandaloneServer(server, { listen: { port: 4001 } });

A second subgraph that contributes fields to the same entity declares the type with the matching @key, marks borrowed fields @external, and adds its own fields. The router fetches the base entity first, then routes an _entities query here to hydrate the contributed fields:

# orders subgraph SDL
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.9",
        import: ["@key", "@external", "@requires"])

type User @key(fields: "id") {
  id: ID!
  email: String! @external
  orderTotal: Float @requires(fields: "email")  # needs email to compute
}

The @requires directive forces the router to fetch email from the owning subgraph before invoking the local resolver for orderTotal. This creates an explicit edge in the query plan. Overusing @requires on deeply nested fields multiplies round trips and memory overhead — keep dependency chains shallow and batch them, as covered in Optimizing Reference Resolvers for Performance.

This two-subgraph pattern generalises to every entity in the graph, and the discipline it encodes is what keeps a federated schema maintainable as the number of subgraphs grows. The owning subgraph is the single source of truth for the identity fields and for the base record; every other subgraph contributes additional fields and borrows only what it strictly needs through @external. When you find yourself wanting to mark many fields @external in a contributing subgraph, that is usually a signal the boundary is wrong — the field probably belongs to the contributor, or the two services share an aggregate that should have been one. The directives are deliberately narrow so that the SDL itself documents the coupling between services: anyone reading the orders subgraph above can see at a glance that it depends on email from elsewhere and computes orderTotal locally. That readability is a feature, and it is worth protecting by keeping the borrowed surface small.

One more property of this pattern matters for production: it is incrementally deployable. Because composition validates the whole supergraph but each subgraph deploys on its own schedule, you can add orderTotal to the orders subgraph, run rover subgraph check, and ship it without touching the accounts subgraph at all — the @external declaration is a contract reference, not a code dependency. That independence is the entire point of federation, and the entity-resolution contract is what makes it safe.

Cross-Section Integration Points

This section produces correct subgraph schemas; two adjacent sections decide what those schemas should be and what happens once they run.

  • Upstream — architecture and ownership. Before you write a @key, you decide which subgraph owns the entity and where the domain boundary falls. Those choices are made in GraphQL Federation Architecture & Design. A clean @key cannot rescue a boundary that splits one aggregate across two services; entity resolution amplifies whatever ownership model you start with.

  • Downstream — production operations. A composed supergraph still has to be deployed, observed, cached, and migrated. Reference-resolver latency only matters once you can see it: distributed tracing, query-plan caching, and entity response caching all live in Federated GraphQL Operations in Production. The @key you choose becomes the cache key for entity caching, so identity decisions here have direct operational consequences there.

  • Shared contracts. Custom scalars and enums used in @key fields or arguments must serialise identically in every subgraph. The governance for that spans both the type-ownership work upstream and the scalar and enum guides in this section.

Common Failure Modes & Composition Errors

These are the failures teams hit most often. Each is detectable before deploy with rover supergraph compose or a subgraph check.

  • Missing @key on an entity referenced elsewhere. A type referenced across subgraphs without a @key is treated as a value type, not an entity, and cannot be resolved across boundaries. Composition reports the type as missing a @key directive. The systematic fix path is in Debugging Missing @key Fields in Apollo Federation v2.

  • @key field type mismatch across subgraphs. Declaring id: ID! in one subgraph and id: String! in another fails composition with a field-type conflict. Standardise on ID! for identity fields everywhere.

  • @external on the owning subgraph. Marking a @key field @external in the subgraph that should own it tells composition that no subgraph owns the field. Only extending subgraphs mark borrowed fields @external.

  • Circular @requires between subgraphs. Two subgraphs that each @requires a field the other computes create a cycle the query planner cannot order, producing a composition error. Break the cycle by moving the computed field or denormalising one dependency.

  • Silent null returns from __resolveReference. A resolver that returns undefined/null for a missing record causes the router to drop the entity branch with no error. Throw an explicit error so failures surface in traces.

The common thread across these is that the loud failures — missing keys, type mismatches, cycles — are the safe ones, because composition refuses to ship them. The dangerous failures are the quiet ones: a resolver that returns null for a record it should have found, an enum value a client sends that one subgraph silently rejects, a @requires field that arrives empty because the owning resolver had a transient error. These pass composition and surface only as partial responses or intermittent nulls in production, often blamed on the client or the network before anyone suspects the entity contract. The defensive posture is the same in every case: make resolvers throw rather than return empty, model cross-service fields as nullable so degradation is intentional, and lean on tracing so a dropped branch shows up as a span rather than a mystery. Each guide in this section closes with the specific quiet failures of its directive and how to make them loud.

CI/CD & Tooling Integration

Composition correctness is a property you enforce in the pipeline, not at runtime. Run rover supergraph compose on every pull request to catch unresolved @external references and field-type mismatches before merge:

# compose locally or in CI from a supergraph config listing each subgraph
rover supergraph compose --config supergraph.yaml > supergraph.graphql

Against a managed registry, validate each subgraph change before it can affect the running graph, then publish on merge:

# check a proposed subgraph schema against the published graph + recent traffic
rover subgraph check "$APOLLO_GRAPH_REF" \
  --name accounts \
  --schema ./accounts/schema.graphql

# publish after the check passes and the change is merged
rover subgraph publish "$APOLLO_GRAPH_REF" \
  --name accounts \
  --schema ./accounts/schema.graphql \
  --routing-url https://accounts.internal/graphql

rover subgraph check validates composition and runs operation checks against recent traffic, so it catches breaking changes to fields clients actually use — not just schema-level conflicts. Wire it into the merge gate so a breaking @key change cannot reach the registry. The schema-validation and managed-federation workflows that wrap these commands are detailed in GraphQL Federation Architecture & Design.

There is a meaningful difference between the two check modes, and using the right one matters. Local rover supergraph compose answers a structural question: do these subgraph schemas, sitting side by side, compose into a valid supergraph at all? It needs no network access and no registry, which makes it ideal as a fast pre-commit hook and as the inner loop while you iterate on SDL. Registry-aware rover subgraph check answers a behavioural question: if I publish this one subgraph’s proposed schema against the graph that is currently live, does composition still succeed, and does any field that real traffic depends on break? It needs an Apollo key and a graph ref, and it is the gate that belongs on the pull request. A healthy pipeline runs both — compose locally for speed, check against the registry for safety — and never lets a subgraph publish until the registry check is green. Treating composition as a unit test for the graph is the single highest-leverage habit a federation team can adopt, because it converts a class of production incidents into a class of red builds.

A practical note on key changes specifically: a @key is the one part of an entity contract that can never be changed casually, because it is simultaneously the routing identity, the merge key, and — once entity caching is enabled in production — the cache key. Renaming a key field, narrowing its type, or switching from a single to a composite key is a breaking change to every subgraph that references the entity and to every cached entity already in flight. Such changes belong in a deliberate, staged migration with @override or a temporary additional @key, not in a routine pull request, and the registry check is what stops them from slipping through unnoticed.

Decision Guide

Use this to choose the right directive for a cross-service requirement before reaching for code.

You need to… Use Notes
Identify an entity for cross-subgraph resolution @key Prefer one stable, low-cardinality field; use composite keys only for partitioned data
Reference a field owned by another subgraph @external Only in the extending subgraph, never the owner
Compute a field that depends on another subgraph’s data @requires Keep chains shallow; each adds a query-plan edge
Avoid an extra hop when you already hold the data @provides Declares a field can supply specific external fields
Let two subgraphs resolve the same non-key field @shareable Values must be consistent across resolvers
Move a field’s ownership between subgraphs @override Supports progressive migration with traffic percentages
Restrict a field to authenticated/scoped requests @authenticated / @requiresScopes / @policy Enforced locally per subgraph from propagated headers

A short pre-deploy checklist for any entity-owning subgraph:

Frequently Asked Questions

How does the router handle entity resolution across multiple subgraphs?

The router decomposes the client operation into an execution plan, fetches base entities from their owning subgraphs, extracts the @key fields into entity representations, dispatches batched _entities queries to subgraphs that contribute additional fields, and merges every branch by shared key. It orders dependent fetches according to @requires edges and returns partial data rather than failing the whole operation when a non-critical branch errors.

What makes a good @key field choice?

Prefer a single, stable, low-cardinality identifier such as a primary id. Avoid mutable fields like email addresses or status flags, because the router uses the key to build representations and as a cache key — a value that changes invalidates caches and can split one logical entity into two. Reserve composite keys for genuinely partitioned data such as region id.

What are the performance implications of @requires on nested fields?

Each @requires adds a node to the query plan and may force a representation fetch before the local resolver runs, increasing latency if the dependency is not batched. Resolve it with DataLoader inside __resolveReference so a batch of keys becomes one data-source call, and keep dependency chains shallow. See Optimizing Reference Resolvers for Performance for the batching patterns.

Can different subgraphs define conflicting @key directives for the same entity?

No. Every subgraph that owns or references an entity must agree on the key fields and their types. Conflicting keys or mismatched scalar types fail composition. Enforce consistency with centralised type governance and rover subgraph check in CI so mismatches are caught before they reach the registry.

How do I handle partial entity failures without breaking the entire query?

Federation supports partial responses. When a subgraph fails to resolve an entity, the router returns null for that branch while preserving successfully resolved data — provided the field is nullable. Model cross-service dependencies as nullable fields, add circuit breakers around external calls, and use fallback resolvers for graceful degradation rather than full-operation failure.

Do I still need @extends like in Federation v1?

No. Federation v2 propagates entity metadata through composition, so a referencing subgraph declares the type with the matching @key and marks borrowed fields @external — there is no manual @extends. The owning subgraph defines the key fields natively, and every owning subgraph must implement its own __resolveReference; composition never synthesises one.

Where should authorization live in a federated graph?

At the field, enforced locally in the subgraph that owns it, using @authenticated, @requiresScopes, or @policy. The router propagates identity context in headers and each subgraph evaluates policy against it, preserving service autonomy while keeping access rules consistent. The patterns are detailed in Directive Patterns for Cross-Service Authorization.