ScriptLabs

ServicesTechnologyPortfolioAboutInsightsContact
Sign InSign Up
HomeInsightsBlogWeb Development
Web Development

GraphQL Federation with Apollo Router: Microservices at Scale

Building unified APIs across distributed services

Fernando A McKenzie
Fernando A McKenzie
Founder
January 1, 2024
9 min read

The Microservices API Challenge

GraphQL Federation enables you to build a unified GraphQL API from multiple underlying services, each managing their own portion of the graph. This approach solves the complex challenge of creating cohesive APIs in microservices architectures while maintaining team autonomy and service independence.

At ScriptLabs Studios, we've implemented GraphQL Federation for enterprises managing dozens of microservices, creating seamless API experiences that scale to hundreds of millions of requests per day.

Understanding Apollo Federation 2.0

Apollo Federation 2.0 introduces powerful composition features that make it easier to build and maintain distributed GraphQL schemas at scale.

Core Federation Concepts

  • Subgraphs - Individual GraphQL services that own specific entities
  • Supergraph - The unified schema composed from all subgraphs
  • Router - The gateway that executes queries across subgraphs
  • Federation directives - Special directives that define relationships
  • Entity resolution - How the router fetches related data

Subgraph Architecture

Users Subgraph

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

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

type UserProfile {
  bio: String
  avatar: String
  location: String
}
// users/resolvers.js
const resolvers = {
  User: {
    __resolveReference(user) {
      return findUserById(user.id);
    },
    
    profile: async (user) => {
      return await fetchUserProfile(user.id);
    }
  },
  
  Query: {
    me: (_, __, { user }) => {
      return user;
    },
    
    users: async (_, { limit, offset }) => {
      return await fetchUsers({ limit, offset });
    }
  },
  
  Mutation: {
    updateProfile: async (_, { input }, { user }) => {
      const updatedUser = await updateUserProfile(user.id, input);
      return updatedUser;
    }
  }
};

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

Products Subgraph

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

type Product @key(fields: "id") {
  id: ID!
  name: String!
  description: String!
  price: Money!
  category: Category!
  reviews: [Review!]!
  createdBy: User! # Reference to Users subgraph
}

type Category @key(fields: "id") {
  id: ID!
  name: String!
  products: [Product!]!
}

type Money {
  amount: Float!
  currency: String!
}

type Review {
  id: ID!
  rating: Int!
  comment: String!
  reviewer: User! # Reference to Users subgraph
  createdAt: DateTime!
}

# Extend User from users subgraph
extend type User @key(fields: "id") {
  id: ID! @external
  createdProducts: [Product!]!
  reviews: [Review!]!
}
// products/resolvers.js
const resolvers = {
  Product: {
    __resolveReference(product) {
      return findProductById(product.id);
    },
    
    category: async (product) => {
      return await fetchCategory(product.categoryId);
    },
    
    reviews: async (product) => {
      return await fetchProductReviews(product.id);
    },
    
    createdBy: (product) => {
      return { __typename: "User", id: product.createdById };
    }
  },
  
  Category: {
    __resolveReference(category) {
      return findCategoryById(category.id);
    },
    
    products: async (category) => {
      return await fetchProductsByCategory(category.id);
    }
  },
  
  User: {
    createdProducts: async (user) => {
      return await fetchProductsByUser(user.id);
    },
    
    reviews: async (user) => {
      return await fetchReviewsByUser(user.id);
    }
  },
  
  Review: {
    reviewer: (review) => {
      return { __typename: "User", id: review.reviewerId };
    }
  },
  
  Query: {
    product: (_, { id }) => findProductById(id),
    products: (_, args) => fetchProducts(args),
    categories: () => fetchAllCategories()
  }
};

Advanced Federation Patterns

Entity Interfaces and Unions

# shared/schema.graphql
interface Node @key(fields: "id") {
  id: ID!
}

union SearchResult = User | Product | Category

# products/schema.graphql
type Product implements Node @key(fields: "id") {
  id: ID!
  name: String!
  # ... other fields
}

# users/schema.graphql  
type User implements Node @key(fields: "id") {
  id: ID!
  email: String!
  # ... other fields
}

# search/schema.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external"])

extend type Query {
  search(query: String!): [SearchResult!]!
  node(id: ID!): Node
}

Computed Fields with @requires

# inventory/schema.graphql
extend type Product @key(fields: "id") {
  id: ID! @external
  price: Money! @external
  
  # Computed field that requires price from products subgraph
  discountedPrice: Money! @requires(fields: "price")
  inStock: Boolean!
  availableQuantity: Int!
}

// inventory/resolvers.js
const resolvers = {
  Product: {
    discountedPrice: (product) => {
      const discount = calculateDiscount(product.price, product.category);
      return {
        amount: product.price.amount * (1 - discount),
        currency: product.price.currency
      };
    },
    
    inStock: async (product) => {
      const inventory = await fetchInventory(product.id);
      return inventory.quantity > 0;
    },
    
    availableQuantity: async (product) => {
      const inventory = await fetchInventory(product.id);
      return inventory.quantity;
    }
  }
};

Apollo Router Configuration

Router Setup with Composition

# router/supergraph.yaml
federation_version: 2
subgraphs:
  users:
    routing_url: http://users-service:4001/graphql
    schema:
      subgraph_url: http://users-service:4001/graphql
  
  products:
    routing_url: http://products-service:4002/graphql
    schema:
      subgraph_url: http://products-service:4002/graphql
  
  inventory:
    routing_url: http://inventory-service:4003/graphql
    schema:
      subgraph_url: http://inventory-service:4003/graphql
  
  reviews:
    routing_url: http://reviews-service:4004/graphql
    schema:
      subgraph_url: http://reviews-service:4004/graphql
# router/router.yaml
supergraph:
  path: ./supergraph-schema.graphql

headers:
  all:
    request:
      - propagate:
          named: "authorization"
      - propagate:
          named: "x-user-id" 

cors:
  origins:
    - "https://app.example.com"
  allow_headers:
    - "content-type"
    - "authorization"

plugins:
  - name: "authentication"
    enabled: true
  - name: "authorization" 
    enabled: true
  - name: "caching"
    enabled: true
    
traffic_shaping:
  all:
    timeout: 30s
    
  subgraphs:
    users:
      timeout: 5s
    products:
      timeout: 10s

Automated Schema Composition

#!/bin/bash
# compose-schema.sh

# Install Rover CLI
npm install -g @apollo/rover

# Compose supergraph schema
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql

if [ $? -eq 0 ]; then
    echo "Schema composition successful"
    
    # Validate the composed schema
    rover graph introspect --schema ./supergraph-schema.graphql
    
    # Start the router
    ./router --config router.yaml --supergraph supergraph-schema.graphql
else
    echo "Schema composition failed"
    exit 1
fi

Performance Optimization

Query Planning and Caching

// router/plugins/caching.js
const cache = new Map();

const cachingPlugin = {
  requestDidStart() {
    return {
      willSendSubgraphRequest({ request, subgraphName }) {
        const cacheKey = subgraphName + ':' + request.query + ':' + JSON.stringify(request.variables);
        
        if (cache.has(cacheKey)) {
          const cachedResponse = cache.get(cacheKey);
          if (Date.now() - cachedResponse.timestamp < 60000) { // 1 minute TTL
            return cachedResponse.response;
          }
        }
        
        return null;
      },
      
      didReceiveSubgraphResponse({ response, subgraphName, request }) {
        if (response.data && !response.errors) {
          const cacheKey = subgraphName + ':' + request.query + ':' + JSON.stringify(request.variables);
          cache.set(cacheKey, {
            response: response.data,
            timestamp: Date.now()
          });
        }
      }
    };
  }
};

DataLoader Implementation

// Implement DataLoader in subgraphs for batching
const DataLoader = require('dataloader');

class ProductService {
  constructor() {
    this.productLoader = new DataLoader(this.batchLoadProducts.bind(this));
    this.categoryLoader = new DataLoader(this.batchLoadCategories.bind(this));
  }
  
  async batchLoadProducts(productIds) {
    const products = await db.product.findMany({
      where: { id: { in: productIds } }
    });
    
    const productMap = products.reduce((map, product) => {
      map[product.id] = product;
      return map;
    }, {});
    
    return productIds.map(id => productMap[id] || null);
  }
  
  async batchLoadCategories(categoryIds) {
    const categories = await db.category.findMany({
      where: { id: { in: categoryIds } }
    });
    
    const categoryMap = categories.reduce((map, category) => {
      map[category.id] = category;
      return map;
    }, {});
    
    return categoryIds.map(id => categoryMap[id] || null);
  }
  
  // Use loaders in resolvers
  async findProductById(id) {
    return this.productLoader.load(id);
  }
  
  async findCategoryById(id) {
    return this.categoryLoader.load(id);
  }
}

const productService = new ProductService();

const resolvers = {
  Product: {
    category: async (product) => {
      return await productService.findCategoryById(product.categoryId);
    }
  }
};

Monitoring and Observability

Distributed Tracing

// monitoring/tracing.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-node');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

const provider = new NodeTracerProvider({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'graphql-federation',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
  }),
});

// Add to Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginInlineTrace(),
    {
      requestDidStart() {
        return {
          willSendSubgraphRequest({ request, subgraphName }) {
            const span = trace.getActiveSpan();
            span?.setAttributes({
              'subgraph.name': subgraphName,
              'graphql.operation.type': request.operationName,
            });
          },
          
          didReceiveSubgraphResponse({ response, subgraphName }) {
            const span = trace.getActiveSpan();
            span?.setAttributes({
              'subgraph.response.errors': response.errors?.length || 0,
            });
          }
        };
      }
    }
  ]
});

Metrics Collection

// monitoring/metrics.js
const promClient = require('prom-client');

const metrics = {
  httpRequestDuration: new promClient.Histogram({
    name: 'graphql_request_duration_seconds',
    help: 'GraphQL request duration',
    labelNames: ['operation', 'subgraph']
  }),
  
  subgraphRequests: new promClient.Counter({
    name: 'graphql_subgraph_requests_total',
    help: 'Total subgraph requests',
    labelNames: ['subgraph', 'status']
  }),
  
  entityResolutions: new promClient.Counter({
    name: 'graphql_entity_resolutions_total', 
    help: 'Total entity resolutions',
    labelNames: ['entity_type']
  })
};

const metricsPlugin = {
  requestDidStart() {
    return {
      willSendSubgraphRequest({ subgraphName }) {
        const timer = metrics.httpRequestDuration.startTimer({
          subgraph: subgraphName
        });
        
        return {
          didReceiveSubgraphResponse({ response }) {
            timer();
            metrics.subgraphRequests.inc({
              subgraph: subgraphName,
              status: response.errors ? 'error' : 'success'
            });
          }
        };
      }
    };
  }
};

Security Implementation

Authentication and Authorization

// auth/middleware.js
const jwt = require('jsonwebtoken');

class AuthenticationService {
  async validateToken(token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      return {
        userId: decoded.sub,
        roles: decoded.roles,
        permissions: decoded.permissions
      };
    } catch (error) {
      throw new Error('Invalid token');
    }
  }
  
  async checkPermission(user, resource, action) {
    return user.permissions.some(permission => 
      permission.resource === resource && 
      permission.actions.includes(action)
    );
  }
}

// Apply to subgraph resolvers
const resolvers = {
  Query: {
    sensitiveData: async (_, args, { user }) => {
      if (!user) {
        throw new Error('Authentication required');
      }
      
      if (!await authService.checkPermission(user, 'sensitive_data', 'read')) {
        throw new Error('Insufficient permissions');
      }
      
      return await fetchSensitiveData(args);
    }
  }
};

Schema-Level Security

# auth/schema.graphql
directive @auth(requires: [String!]) on FIELD_DEFINITION | OBJECT

type User @auth(requires: ["user:read"]) {
  id: ID!
  email: String! @auth(requires: ["user:email"])
  personalData: PersonalData! @auth(requires: ["user:pii"])
}

type AdminPanel @auth(requires: ["admin:access"]) {
  users: [User!]!
  metrics: SystemMetrics!
}

Deployment Strategies

Container Orchestration

# docker-compose.yml
version: '3.8'

services:
  apollo-router:
    image: ghcr.io/apollographql/router:latest
    ports:
      - "4000:4000"
    volumes:
      - ./router/router.yaml:/app/router.yaml
      - ./router/supergraph-schema.graphql:/app/supergraph-schema.graphql
    environment:
      - APOLLO_ROUTER_CONFIG_PATH=/app/router.yaml
      - APOLLO_ROUTER_SUPERGRAPH_PATH=/app/supergraph-schema.graphql
    depends_on:
      - users-service
      - products-service
      - inventory-service

  users-service:
    build: ./services/users
    ports:
      - "4001:4001"
    environment:
      - DATABASE_URL=${USERS_DATABASE_URL}
      - APOLLO_KEY=${APOLLO_KEY}
    
  products-service:
    build: ./services/products
    ports:
      - "4002:4002"
    environment:
      - DATABASE_URL=${PRODUCTS_DATABASE_URL}
      - APOLLO_KEY=${APOLLO_KEY}
    
  inventory-service:
    build: ./services/inventory
    ports:
      - "4003:4003"
    environment:
      - DATABASE_URL=${INVENTORY_DATABASE_URL}
      - APOLLO_KEY=${APOLLO_KEY}

Kubernetes Deployment

# k8s/router-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apollo-router
spec:
  replicas: 3
  selector:
    matchLabels:
      app: apollo-router
  template:
    metadata:
      labels:
        app: apollo-router
    spec:
      containers:
      - name: router
        image: ghcr.io/apollographql/router:latest
        ports:
        - containerPort: 4000
        env:
        - name: APOLLO_ROUTER_CONFIG_PATH
          value: /app/router.yaml
        - name: APOLLO_ROUTER_SUPERGRAPH_PATH
          value: /app/supergraph-schema.graphql
        volumeMounts:
        - name: config
          mountPath: /app
        resources:
          requests:
            memory: "256Mi"
            cpu: "200m"
          limits:
            memory: "512Mi"
            cpu: "500m"
      volumes:
      - name: config
        configMap:
          name: router-config

---
apiVersion: v1
kind: Service
metadata:
  name: apollo-router-service
spec:
  selector:
    app: apollo-router
  ports:
  - protocol: TCP
    port: 80
    targetPort: 4000
  type: LoadBalancer

Testing Federation

Integration Testing

// tests/federation.test.js
const { ApolloServer } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');

describe('Federation Integration Tests', () => {
  let usersServer, productsServer;
  
  beforeAll(async () => {
    // Start subgraph servers
    usersServer = new ApolloServer({
      schema: buildSubgraphSchema({ typeDefs: usersTypeDefs, resolvers: usersResolvers })
    });
    
    productsServer = new ApolloServer({
      schema: buildSubgraphSchema({ typeDefs: productsTypeDefs, resolvers: productsResolvers })
    });
    
    await usersServer.listen({ port: 4001 });
    await productsServer.listen({ port: 4002 });
  });
  
  afterAll(async () => {
    await usersServer.stop();
    await productsServer.stop();
  });
  
  test('should resolve cross-subgraph queries', async () => {
    const query = `
      query GetUserWithProducts($userId: ID!) {
        user(id: $userId) {
          id
          name
          email
          createdProducts {
            id
            name
            price {
              amount
              currency
            }
          }
        }
      }
    `;
    
    const response = await request(app)
      .post('/graphql')
      .send({
        query,
        variables: { userId: '1' }
      });
    
    expect(response.status).toBe(200);
    expect(response.body.data.user).toBeDefined();
    expect(response.body.data.user.createdProducts).toBeDefined();
  });
});

Migration Strategies

From Monolith to Federation

// Migration phases
const migrationPhases = [
  {
    phase: 1,
    description: "Extract Users subgraph",
    steps: [
      "Create users service with GraphQL endpoint",
      "Implement user-related resolvers",
      "Set up federation directives", 
      "Test user queries in isolation"
    ]
  },
  {
    phase: 2, 
    description: "Extract Products subgraph",
    steps: [
      "Create products service",
      "Implement product resolvers",
      "Add User extensions for product relationships",
      "Test federated user-product queries"
    ]
  },
  {
    phase: 3,
    description: "Extract remaining domains",
    steps: [
      "Identify remaining domain boundaries",
      "Extract services incrementally", 
      "Maintain backwards compatibility",
      "Gradual traffic migration"
    ]
  }
];

// Incremental router configuration
const routerConfig = {
  // Start with monolith handling most traffic
  subgraphs: {
    monolith: {
      routing_url: "http://monolith:4000/graphql",
      schema: {
        subgraph_url: "http://monolith:4000/graphql"
      }
    },
    users: {
      routing_url: "http://users-service:4001/graphql", 
      schema: {
        subgraph_url: "http://users-service:4001/graphql"
      }
    }
  }
};

Best Practices

Schema Design Guidelines

  • Entity Ownership - Each entity should be owned by exactly one subgraph
  • Key Fields - Use stable, unique identifiers for @key directives
  • Field Placement - Place fields close to their data source
  • Avoid Deep Nesting - Prefer flatter structures for better performance
  • Consistent Naming - Use consistent field and type names across subgraphs

Performance Considerations

  • Query Planning - Understand how the router plans query execution
  • N+1 Queries - Use DataLoader to batch entity resolutions
  • Caching Strategy - Implement appropriate caching at multiple levels
  • Monitoring - Track subgraph performance and query complexity

Conclusion

GraphQL Federation with Apollo Router provides a powerful solution for building unified APIs across microservices architectures. By enabling teams to own their domain-specific subgraphs while presenting a cohesive API to consumers, Federation strikes the right balance between autonomy and consistency.

The key to successful Federation lies in thoughtful schema design, proper entity modeling, and robust operational practices. With careful planning and implementation, Federation can scale to support the largest and most complex microservices environments.

As organizations continue to adopt microservices architectures, GraphQL Federation represents a mature, production-ready approach to API composition that can grow with your needs while maintaining developer experience and performance.

Ready to implement GraphQL Federation for your microservices architecture? Let's discuss how Apollo Federation can unify your APIs while preserving team autonomy and system scalability.