GraphQL APIs

GraphQL Federation — Multi-Service Schemas with Apollo Federation

By Domain India Team · DomainIndia EngineeringPublished 10 min read
Knowledge base article
Contents (17 sections)

A single GraphQL schema works well until several teams need to change it at once. Federation splits the graph into subgraphs that each team owns and deploys, while clients still see one endpoint. This guide covers when to federate, Apollo Federation 2 schemas, the Apollo Router, and how to run it on your own servers.

Key takeaways

When several teams share one GraphQL schema, it becomes a bottleneck. Federation lets each team own a subgraph; a router composes them into one unified graph. Start monolithic, federate only when team boundaries demand it, keep subgraphs on a private network behind the router, and run schema checks in CI.

When to federate

Don't start here. Federation adds operational complexity.

Stay monolithic when:

  • A single team owns the schema
  • Fewer than about 20 engineers touch GraphQL
  • There are no clear domain boundaries

Federate when:

  • Multiple teams need autonomy over their slice of the graph
  • Domains are genuinely separate (users, orders, inventory)
  • Monolith deploys are getting slow
  • Contributors keep stepping on each other's schema changes

Most companies never need federation, and many that adopt it do so too early.

The architecture

code
Client (React / iOS / Android)
    │
    ▼
[Router] ──► Subgraph: Users (Node.js)
    │    ──► Subgraph: Products (Python)
    │    ──► Subgraph: Orders (Go)
    │    ──► Subgraph: Reviews (PHP)
    │
    └── One endpoint, one unified schema
  • Each subgraph is a full GraphQL server managed by its team
  • The router plans each query, calls the subgraphs and merges the responses
  • Subgraphs can add fields to each other's entity types
  • The router is the natural place for auth, rate limiting and caching

Tools: Apollo Router and Rover

bash
# Apollo Router (a Rust binary, for production)
curl -sSL https://router.apollo.dev/download/nix/latest | sh

# Rover CLI (composes the supergraph and runs schema checks)
curl -sSL https://rover.apollo.dev/nix/latest | sh

Apollo Gateway (a Node.js library) still exists but Apollo recommends the Router for new projects. Open-source Federation-compatible routers such as GraphQL Hive Gateway and WunderGraph Cosmo are alternatives. Check the Router's licence and which features need a paid GraphOS plan before you design around them.

Step 1 — Design subgraphs

Federation 2 schemas declare the directives they use with @link. Users subgraph:

graphql
# users/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])

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

type Query {
  me: User
  user(id: ID!): User
}

Orders subgraph:

graphql
# orders/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])

type Order @key(fields: "id") {
  id: ID!
  total: Float!
  status: String!
  user: User!           # cross-subgraph reference
}

# Entity stub: Users owns User; Orders adds the orders field
type User @key(fields: "id") {
  id: ID!
  orders: [Order!]!
}

type Query {
  order(id: ID!): Order
}

@key marks a type as an entity that other subgraphs can reference and extend. In Federation 2 you no longer need extend type or @external on key fields.

Step 2 — Subgraph server (Node.js example)

bash
npm install @apollo/server @apollo/subgraph graphql graphql-tag
typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { readFileSync } from 'node:fs';
import gql from 'graphql-tag';

const typeDefs = gql(readFileSync('./schema.graphql', 'utf-8'));

const resolvers = {
  Query: {
    me: async (_, __, { user }) => db.user.findById(user.id),
    user: async (_, { id }) => db.user.findById(id),
  },
  User: {
    __resolveReference: async (ref) => db.user.findById(ref.id),
  },
};

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

const { url } = await startStandaloneServer(server, {
  listen: { port: 4001, host: '10.0.0.2' },   // private address only
  context: async ({ req }) => ({ user: decodeUser(req) }),
});

console.log(`Users subgraph at ${url}`);

__resolveReference: when the router holds {__typename: "User", id: "42"}, it asks the Users subgraph to resolve the full User.

Step 3 — Compose and run the router

supergraph.yaml tells Rover where each subgraph lives and where its schema file is:

yaml
federation_version: =2.5.0
subgraphs:
  users:
    routing_url: http://10.0.0.2:4001/graphql
    schema:
      file: ./users/schema.graphql
  orders:
    routing_url: http://10.0.0.3:4002/graphql
    schema:
      file: ./orders/schema.graphql
  products:
    routing_url: http://10.0.0.4:4003/graphql
    schema:
      file: ./products/schema.graphql

Compose the supergraph schema:

bash
rover supergraph compose --config supergraph.yaml > supergraph.graphql

router.yaml (router behaviour; key names can change between Router versions, so check the docs for yours):

yaml
supergraph:
  listen: 127.0.0.1:4000      # nginx with TLS in front
  path: /graphql

cors:
  origins:
    - https://yourcompany.com

headers:
  all:
    request:
      - propagate:
          named: authorization    # forward the auth header to subgraphs

traffic_shaping:
  router:
    global_rate_limit:
      capacity: 100
      interval: 1s

Start the router:

bash
router --config router.yaml --supergraph supergraph.graphql

Step 4 — Client queries

Clients call the router endpoint only:

graphql
query MyDashboard {
  me {
    id
    name
    orders {         # owned by the Orders subgraph
      id
      total
      status
    }
  }
}

The router:

  1. Queries Users for {id, name}
  2. Queries Orders with the user reference for {id, total, status}
  3. Merges the response

The client doesn't know there are multiple services.

Cross-subgraph relationships

Extending another team's entity

graphql
# Reviews subgraph
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])

type Review @key(fields: "id") {
  id: ID!
  rating: Int!
  text: String!
  product: Product!
}

type Product @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

The Products subgraph knows nothing about reviews. The Reviews subgraph adds a reviews field to Product, and the router sends product.reviews to Reviews.

@requires

When a subgraph needs a field that another subgraph owns (not the key) to compute its own field, mark it @external and use @requires on the same type:

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

type User @key(fields: "id") {
  id: ID!
  email: String! @external
  receiptEmail: String! @requires(fields: "email")
  orders: [Order!]!
}

Before resolving receiptEmail, the router fetches email from the Users subgraph and passes it to the Orders subgraph's __resolveReference. @provides is the opposite optimisation: a subgraph declares it can return some external fields itself, saving a hop.

Auth in federation

Validate the JWT once at the router and forward the header or claims to subgraphs:

yaml
# router.yaml
authentication:
  router:
    jwt:
      jwks:
        - url: https://auth.yourcompany.com/.well-known/jwks.json

Subgraphs can trust what the router forwards only if nothing else can reach them. Keep subgraphs on private addresses with firewall rules that allow only the router; if that isn't possible, verify the JWT in every subgraph too. Check whether the router features you rely on (JWT authentication among them) are available on your Apollo plan.

Caching

The router caches query plans in memory:

yaml
supergraph:
  query_planning:
    cache:
      in_memory:
        limit: 512

Subgraphs built on Apollo Server can set cache hints with @cacheControl (declare the directive in the schema first):

graphql
type Product @key(fields: "id") @cacheControl(maxAge: 60) {
  id: ID!
  name: String!
  price: Float! @cacheControl(maxAge: 10)    # more volatile
}

How those hints turn into cached responses (router response caching, a CDN, or neither) depends on your router version and plan, so test it rather than assume it.

Monitoring

Apollo GraphOS Studio (has a free tier; check current plans) shows:

  • Subgraph latency
  • Error rates per field
  • Slow query hotspots
  • Schema usage (which fields clients actually query)

The self-hosted alternative is OpenTelemetry from the router and subgraphs into Grafana Tempo, alongside Prometheus metrics. See our production observability guide.

Migration from a monolith

A gradual path:

  1. Keep the monolithic GraphQL schema on one server
  2. Put the router in front of it, with the monolith as the only subgraph
  3. When a team needs autonomy, move its types into a new subgraph
  4. Repeat: extract more subgraphs as the monolith shrinks
  5. Stop when the ownership boundaries feel right; you don't have to split everything

Don't "big bang" migrate. Go step by step with clear ownership.

Running this on Domain India

A federated graph is a set of long-running services, so plan it for VPS servers.

code
VPS 1: Apollo Router       (:4000, behind nginx + TLS)
VPS 2: Users subgraph      (:4001) + PostgreSQL
VPS 3: Orders subgraph     (:4002) + PostgreSQL
VPS 4: Products subgraph   (:4003) + PostgreSQL
  • Domain India VPS plans are self-managed KVM servers with full root access. Run each subgraph and the router as systemd services, on one VPS while you are small or on several as you grow.
  • Only the router should be public. Connect the servers over an encrypted tunnel such as WireGuard and allow subgraph ports only from the router's address. Ask support before you rely on any private networking between VPSs.
  • Shared hosting can't run the Router binary or other long-running services. A single Node.js GraphQL server may run under the Node.js app tool on cPanel or DirectAdmin, but that is a monolith, not a federated setup.
  • The App Platform auto-detects Node.js apps, so it can host a Node.js subgraph; other languages or the router need your own Dockerfile. It has no WebSocket support, so GraphQL subscriptions over WebSockets won't work there.

Common pitfalls

Circular dependencies
A requires B which requires A. Draw the entity dependency graph before you split.
@key on a non-unique field
Lookups return the wrong records. Always key on a real unique ID.
Chatty resolvers
One query fires dozens of subgraph requests. Batch with DataLoader inside each subgraph.
Federation without need
Three teams with a federated graph is usually premature. Revisit when ownership really conflicts.
No schema checks
A subgraph deploy breaks the supergraph. Run rover subgraph check (or your router vendor's equivalent) in CI.
Latency stacking
Each hop adds time. Measure p95 per subgraph and fix the slowest first.

FAQ

Apollo Federation or schema stitching?

Schema stitching (from graphql-tools) is still maintained and fine for combining a few schemas you control. Apollo Federation is the more common choice when several teams own separate parts of a graph, because ownership is declared in each subgraph's schema. For new multi-team graphs, federation is the usual choice.

Can I use Hasura Remote Schemas instead of federation?

Yes. Hasura can combine remote GraphQL endpoints with its own database-generated API, which is simpler for mixed stacks. Apollo Federation gives finer control over entity ownership but requires discipline with keys and schema checks.

Should I federate from day one?

No. Start with a monolithic schema and federate only when two or more teams need to evolve their parts of the schema independently.

Can I use federation with a single subgraph?

Yes. One subgraph behind the router still gives you the router's features, such as header handling, rate limiting and query-plan caching, and makes later splitting easier.

What is the REST-based alternative to federation?

The Backend-for-Frontend (BFF) pattern: one API per client type (web, mobile) that aggregates calls to internal REST services. For some teams it is simpler to operate than a federated graph.

Can I run a federated GraphQL stack on shared hosting?

No. The router and subgraphs are long-running services that shared hosting stops. Use one or more VPS servers; a single Node.js subgraph can also run on the App Platform.

Ready to deploy your graph? Compare VPS plans and the App Platform, or open a support ticket if you are unsure which fits.

Host a federated GraphQL stack

Self-managed KVM VPS with full root access and NVMe storage, from ₹553 a month excluding GST.

Explore VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
GraphQL Federation with Apollo — Multi-Service Schemas