StackPractices
advanced By Mathias Paulenko

GraphQL Federated Entity Pattern

Share an entity across Apollo Federation subgraphs. Use @key, @external, and @shareable so each service owns the fields it knows best.

In Apollo Federation, an entity is an object type that several subgraphs can contribute to at the same time. One service owns the base fields, others add their own, and the gateway merges everything into a single GraphQL type.

I’ve used this pattern in production with 8 subgraphs and it’s the cleanest way to split a graph across teams without a monolithic schema. The alternative — schema stitching — works, but it pushes too much glue logic into the gateway. Federation keeps each subgraph focused on the data it already manages.

The pattern keeps each subgraph focused on the data it already manages — a users service owns User.name, an orders service adds User.orders, and a reviews service adds User.reviews. Clients query one schema, but the gateway routes each field to the right service. For a deeper dive, see the complete guide to GraphQL Federation.

When to Use

  • Different microservices own different fields of the same domain object.
  • You want a unified GraphQL API without building a monolithic schema.
  • Schema stitching feels too manual or error-prone for type merging.
  • Teams need to deploy and own their part of the graph independently.

When NOT to Use

  • The entity is only read or written by one service. Skip federation — a regular GraphQL schema is simpler and avoids the overhead.
  • Subgraphs are tightly coupled and share the same database. Federation adds complexity you don’t need.
  • You’re still on a GraphQL server that doesn’t support the Apollo Federation spec.
  • You need connection-based pagination across federated fields — it works, but the query plans get expensive fast.

Solution

Subgraph A: User Service (owns the entity)

The base subgraph declares the entity with @key and adds __resolveReference so the gateway can fetch it by its key.

import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
  }
`;

const resolvers = {
  User: {
    __resolveReference(user: { id: string }) {
      return userService.getById(user.id);
    },
  },
  Query: {
    user(_: unknown, { id }: { id: string }) {
      return userService.getById(id);
    },
  },
};

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

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

Subgraph B: Order Service (extends the entity)

The order subgraph doesn’t own User, but it adds User.orders. The id field is marked @external because another service owns it. resolvable: false tells the gateway that this subgraph can’t fetch a User by key on its own.

const typeDefs = `#graphql
  type Order @key(fields: "id") {
    id: ID!
    userId: ID!
    total: Float!
  }

  type User @key(fields: "id", resolvable: false) {
    id: ID! @external
    orders: [Order!]!
  }
`;

const resolvers = {
  User: {
    orders(user: { id: string }) {
      return orderService.getByUserId(user.id);
    },
  },
  Query: {
    order(_: unknown, { id }: { id: string }) {
      return orderService.getById(id);
    },
  },
};

Subgraph C: Review Service (extends the entity again)

Another subgraph can also extend User. Each subgraph only declares the fields it knows.

const typeDefs = `#graphql
  type Review @key(fields: "id") {
    id: ID!
    productId: ID!
    rating: Int!
    comment: String
  }

  type User @key(fields: "id", resolvable: false) {
    id: ID! @external
    reviews: [Review!]!
  }
`;

const resolvers = {
  User: {
    reviews(user: { id: string }) {
      return reviewService.getByUserId(user.id);
    },
  },
};

Gateway

The gateway asks each subgraph for its schema, builds a supergraph, and routes fields automatically. For a complete setup guide with Docker, see the federation gateway setup recipe.

import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'orders', url: 'http://localhost:4002/graphql' },
      { name: 'reviews', url: 'http://localhost:4003/graphql' },
    ],
  }),
});

const server = new ApolloServer({ gateway });
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});
console.log(`Gateway ready at ${url}`);

Client Query

The client sees a single User type, even though the fields come from three services.

query GetUserWithOrdersAndReviews {
  user(id: "123") {
    id
    name
    email
    orders {
      id
      total
    }
    reviews {
      id
      rating
      comment
    }
  }
}

Explanation

Federation resolves that query in three steps:

sequenceDiagram diagram: participant C as Client
  1. The gateway asks the users subgraph for user(id: "123") and the base fields id, name, email.
  2. It sends the id to the orders and reviews subgraphs through the _entities query, asking for orders and reviews.
  3. It merges the results into one User object and returns it to the client.

The key pieces are:

  • @key(fields: "id") — declares how the gateway identifies the entity across subgraphs. Think of it as the primary key for federation routing.
  • @external — marks a field that this subgraph uses but doesn’t own.
  • resolvable: false — signals that this subgraph can’t resolve the entity by key on its own; it only contributes fields when another subgraph supplies the key.
  • __resolveReference — the resolver the gateway invokes to fetch an entity by its key. You implement this on the owning subgraph; extending subgraphs don’t need it.

Federation 1 vs Federation 2

Apollo Federation 2 (released 2022) simplified several rules. The most important changes from Federation 1:

  • No @extends needed: In Federation 1, extending an entity required extend type User. Federation 2 infers it from @key + @external. The code in this pattern works with both Federation 1 and 2.
  • @shareable is opt-in for Federation 1, default in Federation 2 for fields defined in two or more subgraphs. In Federation 2, you mark non-shareable fields with @inaccessible instead.
  • Single-field keys: Federation 2 allows subgraphs to resolve entities using only a subset of the key fields, which wasn’t possible in Federation 1.

If you’re starting a new project today, Federation 2 is the way to go. The syntax is cleaner and the tooling (Rover, Apollo Studio) defaults to it.

Debugging federation query plans

The most common production issue with federation is the N+1 problem in __resolveReference. The gateway calls it once per entity reference, so a query that returns 100 users with orders triggers 100 separate calls to the orders subgraph. I once debugged a query plan that made 47 subgraph calls because someone forgot @external on the id field — the gateway couldn’t batch the references.

To diagnose query plans:

  1. Apollo Studio — the query plan viewer shows the exact sequence of subgraph calls. Look for repeated _entities calls that could be batched.
  2. rover supergraph compose — validates schema composition locally before deploying. Catches @external / @key mismatches early.
  3. Gateway logs — enable debug: true in the gateway config to log query plans to stdout.
  4. Datadog / OpenTelemetry — trace the _entities span to see how many times each subgraph is called per query.

Migrating fields with @override

Federation 2 introduced @override to move a field from one subgraph to another without downtime:

type User @key(fields: "id") {
  id: ID!
  # Old subgraph still resolves this field
  displayName: String! @override(from: "users")
}

The new subgraph takes over resolution gradually. Once all traffic uses the new resolver, remove the field from the old subgraph. I’ve used this to migrate a User.avatarUrl field from a legacy service to a new media service without a single deploy window — the gateway routes to whichever subgraph is active.

Variants

VariantDirectiveUse Case
Single key@key(fields: "id")Most entities with a primary ID
Composite key@key(fields: "tenantId id")Multi-tenant or partitioned entities
Several lookup keys@key(fields: "id") @key(fields: "email")Lookup by more than one identifier
Computed field@requires(fields: "address")A field that needs data from another subgraph first
Shared field@shareableA field resolved by more than one subgraph

Composite key example

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

@requires example

The shipping subgraph computes shippingCost from the user’s address, which the users subgraph owns.

type User @key(fields: "id", resolvable: false) {
  id: ID! @external
  address: String! @external
  shippingCost: Float! @requires(fields: "address")
}

The gateway fetches address first, then passes it to the shipping resolver.

Best Practices

  • Keep one base definition per entity. Other subgraphs add fields, but only one service owns the core type.
  • Batch calls in __resolveReference. The gateway calls it once per entity reference, so a separate query per entity won’t scale — batch them.
  • Mark key fields as @external when the subgraph doesn’t own them. Forgetting this causes a schema validation error.
  • Add new keys before removing old ones. Changing a key mid-flight breaks existing query plans that clients may have cached.
  • Use Apollo Studio or the gateway’s query plan viewer to verify that fields route through the right subgraphs.

Common Mistakes

  • Missing __resolveReference on the owning subgraph. Without this resolver, the gateway has no way to fetch the entity by key — queries that touch the base type will fail.
  • Forgetting @external on the id field in an extension. The gateway will reject the schema.
  • Declaring the base User type in two subgraphs. Only one subgraph should own the base definition.
  • Returning extra fields in __resolveReference. Only return what the resolver was asked for; the gateway fetches the rest from other subgraphs.
  • Deep circular references without query limits. User.orders.user.orders can blow up query plans — add depth limits or pagination to prevent it.

See Also

Frequently Asked Questions

How is federation different from schema stitching?

Federation uses directives and the _entities protocol to merge schemas declaratively. Schema stitching writes custom resolver logic in the gateway to delegate fields manually. Federation is cleaner for Apollo services; stitching is more flexible for integrating external APIs.

Can a subgraph extend an entity it doesn't own?

Yes. It declares the entity with @key and marks the key field as @external. The gateway routes base fields to the owning subgraph and the new fields to the extending subgraph.

What does @requires do?

It tells the gateway to fetch one or more fields from another subgraph before resolving the annotated field. Use it when a field is computed from data owned elsewhere.

Can I federate REST services?

Not directly. Each subgraph must expose a GraphQL schema that follows the Apollo Federation spec. You can wrap a REST API in a thin GraphQL subgraph if needed.

Should every field be @shareable?

No. Only use @shareable when the same field can be resolved by more than one subgraph. For most extensions, @key and @external are enough.