Client Area
GraphQL APIsAdvanced

GraphQL Federation — Multi-Service Schemas with Apollo Federation

ByDomain India Team·DomainIndia Engineering
4 min read24 Apr 20263 views
# GraphQL Federation — Multi-Service Schemas with Apollo Federation
TL;DR
When your app grows past 5-10 teams, a single monolithic GraphQL schema becomes a bottleneck. Federation lets each team own a subgraph; a gateway composes them into a unified graph. This guide covers Apollo Federation v2 setup, subgraph design, gateway deployment on DomainIndia VPS, and common pitfalls.
## When to federate Don't start here. Federation adds operational complexity. **Stay monolithic when:** - Single team owns the schema - <20 engineers touching GraphQL - 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 step on each other's schemas Most companies never need federation. Most that adopt it, adopt too early. ## The architecture ``` Client (React / iOS / Android) │ ▼ [Gateway / 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 - Gateway queries subgraphs, composes responses - Subgraphs can extend each other's types - Gateway handles auth, rate limiting, caching ## Apollo Federation v2 Install Apollo Gateway + Router: ```bash # Router (Rust, high performance — recommended for production) curl -sSL https://router.apollo.dev/download/nix/latest | sh ``` Alternative: Apollo Gateway (Node.js — simpler for development). ## Step 1 — Design subgraphs Users subgraph: ```graphql # users/schema.graphql type User @key(fields: "id") { id: ID! email: String! name: String! createdAt: String! } extend type Query { me: User user(id: ID!): User } ``` Orders subgraph: ```graphql # orders/schema.graphql type Order @key(fields: "id") { id: ID! total: Float! status: String! user: User! # cross-subgraph reference } extend type User @key(fields: "id") { id: ID! @external # declared in Users subgraph orders: [Order!]! # new field on User owned by Orders subgraph } extend type Query { order(id: ID!): Order } ``` `@key` marks a type as federatable. `extend type` adds fields to a type owned by another subgraph. ## Step 2 — Subgraph server (Node.js example) ```bash npm install @apollo/subgraph graphql ``` ```typescript import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { buildSubgraphSchema } from '@apollo/subgraph'; import { readFileSync } from 'fs'; import { parse } from 'graphql'; const typeDefs = parse(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 }, context: async ({ req }) => ({ user: decodeUser(req) }), }); console.log(`Users subgraph at ${url}`); ``` `__resolveReference` — when the gateway has a `{__typename: "User", id: "42"}`, it asks the Users subgraph to resolve full User. ## Step 3 — Gateway (Apollo Router) `router.yaml`: ```yaml supergraph: listen: 0.0.0.0:4000 path: /graphql subgraphs: users: routing_url: http://users-service.internal:4001/graphql orders: routing_url: http://orders-service.internal:4002/graphql products: routing_url: http://products-service.internal:4003/graphql cors: origins: - https://yourcompany.com headers: all: request: - propagate: named: authorization # forward auth header to subgraphs rate_limiting: global: interval: 1s capacity: 100 ``` Compose the supergraph SDL: ```bash rover supergraph compose --config supergraph.yaml > supergraph.graphql ``` Start router: ```bash router --config router.yaml --supergraph supergraph.graphql ``` ## Step 4 — Client queries Clients hit the gateway endpoint only: ```graphql query MyDashboard { me { id name orders { # owned by Orders subgraph id total status } } } ``` The gateway orchestrates: 1. Query Users for `{id, name}` 2. Query Orders with the user ID for `{id, total, status}` 3. Compose response Client doesn't know there are multiple services. ## Cross-subgraph relationships ### Key entities ```graphql # Reviews subgraph type Review @key(fields: "id") { id: ID! rating: Int! text: String! product: Product! } extend type Product @key(fields: "id") { id: ID! @external reviews: [Review!]! } ``` Products subgraph doesn't know about Reviews. Reviews subgraph extends Product with a `reviews` field. The gateway routes `product.reviews` to the Reviews subgraph. ### @requires and @provides When Orders needs a User field that's NOT the key: ```graphql extend type User @key(fields: "id") { id: ID! @external email: String @external } type Order @key(fields: "id") { id: ID! notificationEmail: String @requires(fields: "email") user: User! } ``` `@requires(fields: "email")` — before resolving `notificationEmail`, gateway fetches `email` from Users subgraph. ## Deployment on DomainIndia Each subgraph as its own systemd service on a VPS (or separate VPS). Router as separate service. ``` VPS 1: Apollo Router (:4000) VPS 2: Users subgraph (:4001) + Postgres-users VPS 3: Orders subgraph (:4002) + Postgres-orders VPS 4: Products subgraph (:4003) + Postgres-products ``` Internal network (Cloudflare Tunnel or private subnet) for subgraph traffic. Only router exposed publicly. ## Auth in federation JWT validated at router, claims forwarded to subgraphs: ```yaml # router.yaml authentication: router: jwt: jwks: - url: https://auth.yourcompany.com/.well-known/jwks.json ``` Subgraphs trust the router's auth context — don't re-verify JWT per subgraph. ## Caching Apollo Router has query-plan caching: ```yaml supergraph: query_planning: cache: in_memory: limit: 512 ``` Responses can be cached per-field with `@cacheControl(maxAge: 60)`: ```graphql type Product @key(fields: "id") @cacheControl(maxAge: 60) { id: ID! name: String! price: Float! @cacheControl(maxAge: 10) # more volatile } ``` ## Monitoring Apollo Studio (paid — free tier) gives visibility: - Subgraph latency - Error rates per field - Slow query hotspots - Schema usage (which fields are actually queried) Alternative: self-hosted with OpenTelemetry → Grafana Tempo. ## Migration from monolith Gradual path: 1. Start with a monolithic GraphQL schema on 1 server 2. When a team needs autonomy, extract their types into a subgraph 3. Add the first subgraph router between clients and the monolith 4. Repeat: extract more subgraphs, monolith shrinks 5. Eventually: pure federated architecture Don't "big bang" migrate. Incremental with clear ownership. ## Common pitfalls ## FAQ
Q Apollo Federation or GraphQL stitching?

Stitching is legacy (Apollo v1 approach). Federation v2 is the modern standard. New projects use federation.

Q Hasura Remote Schemas instead of federation?

Hasura federates any GraphQL endpoint — simpler for mixed technology stacks. Apollo Federation gives more control (requires @key discipline).

Q Monolith or federate from day one?

Monolith. Federate only when 2+ teams independently evolve the schema.

Q Can I use Federation with a single subgraph?

Yes — even one subgraph + router gets you router features (rate limiting, auth, caching). Pragmatic starting point.

Q REST alternative to federation?

BFF pattern (Backend-for-Frontend) — one GraphQL API per client (web, mobile). Each BFF aggregates REST calls. Simpler ops than federation for some teams.

Host a federated GraphQL stack on multiple DomainIndia VPSes. Explore VPS plans

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket