GraphQL Gateway

Unified API Gateway for Microservices Architecture

๐ŸŒ Project Overview

A GraphQL Gateway that unifies multiple microservices into a single, cohesive API endpoint. This architecture implements Apollo Federation and Schema Stitching to create a distributed graph that allows clients to query data from multiple services through one unified interface.

๐ŸŽฏ Key Objectives

  • Unified API: Single entry point for all microservices
  • Service Independence: Each service maintains its own schema and resolvers
  • Cross-Service Queries: Query data across multiple services in one request
  • Type Safety: Strong typing across the entire distributed graph
  • Scalability: Independently scalable microservices

๐Ÿ—๏ธ Gateway Architecture

GraphQL Gateway

Apollo Server + Federation

Users Service

Port 4001

Products Service

Port 4002

Orders Service

Port 4003

Reviews Service

Port 4004

PostgreSQL

MongoDB

Redis Cache

4
Microservices
1
Unified Gateway
50+
GraphQL Types
99.9%
Uptime SLA

๐Ÿ›๏ธ Microservices Architecture

The gateway implements a federated architecture where each microservice is responsible for its own domain and can be developed, deployed, and scaled independently.

๐Ÿ”—

Apollo Federation

Declarative composition of distributed GraphQL schemas using the @key, @extends, and @external directives for seamless type sharing.

๐Ÿงฉ

Schema Stitching

Alternative approach using @graphql-tools/stitch to merge multiple schemas into a unified gateway schema with custom type merging.

โšก

Service Discovery

Dynamic service registration and health checks using Consul for automatic failover and load balancing across service instances.

๐Ÿ”„

Data Loaders

Batching and caching layer using DataLoader to prevent N+1 queries when resolving cross-service relationships.

๐Ÿ›ก๏ธ

Error Handling

Centralized error aggregation with service-specific error codes, retry logic, and graceful degradation for service failures.

๐Ÿ“Š

Monitoring

Distributed tracing with Jaeger, metrics collection with Prometheus, and real-time dashboard with Grafana.

๐Ÿ”ง Microservices Overview

Each microservice is a standalone GraphQL API with its own database, business logic, and schema. The gateway federates these services into a unified graph.

๐Ÿ‘ฅ Users Service

:4001

Responsibility: User authentication, profiles, and account management

Database: PostgreSQL

Key Types:

  • User - User profile and authentication
  • Address - Shipping and billing addresses
  • PaymentMethod - Saved payment information

Operations: Register, login, updateProfile, changePassword

๐Ÿ“ฆ Products Service

:4002

Responsibility: Product catalog, inventory, and categories

Database: MongoDB

Key Types:

  • Product - Product details and specifications
  • Category - Product categorization
  • Inventory - Stock levels and availability

Operations: searchProducts, getProduct, updateInventory

๐Ÿ›’ Orders Service

:4003

Responsibility: Order processing, checkout, and fulfillment

Database: PostgreSQL

Key Types:

  • Order - Order details and status
  • OrderItem - Individual items in order
  • Payment - Payment transactions

Operations: createOrder, updateStatus, processPayment

โญ Reviews Service

:4004

Responsibility: Product reviews, ratings, and user feedback

Database: MongoDB

Key Types:

  • Review - Product reviews and ratings
  • Rating - Aggregate product ratings
  • Comment - Review comments and replies

Operations: createReview, updateReview, deleteReview

๐Ÿงต Schema Stitching

Schema stitching merges multiple GraphQL schemas into a single unified schema. This approach allows the gateway to present a cohesive API while delegating execution to individual services.

Gateway Schema Configuration

// gateway/server.js
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
const { ApolloServer } = require('apollo-server');

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'products', url: 'http://localhost:4002/graphql' },
      { name: 'orders', url: 'http://localhost:4003/graphql' },
      { name: 'reviews', url: 'http://localhost:4004/graphql' }
    ],
    pollIntervalInMs: 10000 // Poll for schema changes every 10s
  }),
  debug: true,
  serviceHealthCheck: true
});

const server = new ApolloServer({
  gateway,
  context: ({ req }) => ({
    headers: req.headers,
    userId: req.headers['x-user-id']
  }),
  formatError: (error) => {
    console.error('Gateway Error:', error);
    return {
      message: error.message,
      code: error.extensions?.code,
      service: error.extensions?.serviceName
    };
  }
});

server.listen(4000).then(({ url }) => {
  console.log(`๐Ÿš€ Gateway ready at ${url}`);
});

Users Service Schema

// users-service/schema.js
const { gql } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');

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

  type User @key(fields: "id") {
    id: ID!
    email: String!
    username: String!
    firstName: String!
    lastName: String!
    createdAt: String!
    addresses: [Address!]!
    paymentMethods: [PaymentMethod!]!
  }

  type Address {
    id: ID!
    street: String!
    city: String!
    state: String!
    zipCode: String!
    country: String!
    isDefault: Boolean!
  }

  type PaymentMethod {
    id: ID!
    type: PaymentType!
    last4: String!
    expiryMonth: Int
    expiryYear: Int
    isDefault: Boolean!
  }

  enum PaymentType {
    CREDIT_CARD
    DEBIT_CARD
    PAYPAL
  }

  extend type Query {
    me: User
    user(id: ID!): User
    users(limit: Int, offset: Int): [User!]!
  }

  extend type Mutation {
    register(input: RegisterInput!): AuthPayload!
    login(email: String!, password: String!): AuthPayload!
    updateProfile(input: UpdateProfileInput!): User!
    addAddress(input: AddressInput!): Address!
    addPaymentMethod(input: PaymentMethodInput!): PaymentMethod!
  }

  input RegisterInput {
    email: String!
    password: String!
    username: String!
    firstName: String!
    lastName: String!
  }

  input UpdateProfileInput {
    username: String
    firstName: String
    lastName: String
  }

  input AddressInput {
    street: String!
    city: String!
    state: String!
    zipCode: String!
    country: String!
    isDefault: Boolean
  }

  input PaymentMethodInput {
    type: PaymentType!
    cardNumber: String!
    expiryMonth: Int!
    expiryYear: Int!
    cvv: String!
    isDefault: Boolean
  }

  type AuthPayload {
    token: String!
    user: User!
  }
`;

const resolvers = {
  User: {
    __resolveReference(user, { dataSources }) {
      return dataSources.usersAPI.getUserById(user.id);
    }
  },
  Query: {
    me: (_, __, { userId, dataSources }) => {
      return dataSources.usersAPI.getUserById(userId);
    },
    user: (_, { id }, { dataSources }) => {
      return dataSources.usersAPI.getUserById(id);
    },
    users: (_, { limit, offset }, { dataSources }) => {
      return dataSources.usersAPI.getUsers(limit, offset);
    }
  }
};

module.exports = buildSubgraphSchema([{ typeDefs, resolvers }]);

Products Service Schema

// products-service/schema.js
const { gql } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');

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

  type Product @key(fields: "id") {
    id: ID!
    name: String!
    description: String!
    price: Float!
    category: Category!
    images: [String!]!
    specifications: [Specification!]!
    inventory: Inventory!
    averageRating: Float # Computed from reviews service
    reviewCount: Int     # Computed from reviews service
  }

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

  type Inventory {
    quantity: Int!
    status: StockStatus!
    lastUpdated: String!
  }

  type Specification {
    key: String!
    value: String!
  }

  enum StockStatus {
    IN_STOCK
    LOW_STOCK
    OUT_OF_STOCK
    DISCONTINUED
  }

  extend type Query {
    product(id: ID!): Product
    products(
      categoryId: ID
      search: String
      minPrice: Float
      maxPrice: Float
      limit: Int
      offset: Int
    ): ProductConnection!
    category(id: ID!): Category
    categories: [Category!]!
  }

  type ProductConnection {
    edges: [ProductEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }

  type ProductEdge {
    node: Product!
    cursor: String!
  }

  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
  }

  extend type Mutation {
    createProduct(input: CreateProductInput!): Product!
    updateProduct(id: ID!, input: UpdateProductInput!): Product!
    updateInventory(productId: ID!, quantity: Int!): Inventory!
  }

  input CreateProductInput {
    name: String!
    description: String!
    price: Float!
    categoryId: ID!
    images: [String!]!
    specifications: [SpecificationInput!]
    initialQuantity: Int!
  }

  input UpdateProductInput {
    name: String
    description: String
    price: Float
    categoryId: ID
    images: [String!]
    specifications: [SpecificationInput!]
  }

  input SpecificationInput {
    key: String!
    value: String!
  }
`;

const resolvers = {
  Product: {
    __resolveReference(product, { dataSources }) {
      return dataSources.productsAPI.getProductById(product.id);
    }
  },
  Query: {
    product: (_, { id }, { dataSources }) => {
      return dataSources.productsAPI.getProductById(id);
    },
    products: (_, args, { dataSources }) => {
      return dataSources.productsAPI.getProducts(args);
    }
  }
};

module.exports = buildSubgraphSchema([{ typeDefs, resolvers }]);

Cross-Service Type Extensions

// orders-service/schema.js
const typeDefs = gql`
  # Extend User from users-service
  extend type User @key(fields: "id") {
    id: ID! @external
    orders: [Order!]!
  }

  # Extend Product from products-service
  extend type Product @key(fields: "id") {
    id: ID! @external
    name: String! @external
    price: Float! @external
  }

  type Order @key(fields: "id") {
    id: ID!
    user: User!
    items: [OrderItem!]!
    total: Float!
    status: OrderStatus!
    paymentStatus: PaymentStatus!
    shippingAddress: Address!
    createdAt: String!
    updatedAt: String!
  }

  type OrderItem {
    product: Product!
    quantity: Int!
    price: Float!
    subtotal: Float!
  }

  enum OrderStatus {
    PENDING
    CONFIRMED
    PROCESSING
    SHIPPED
    DELIVERED
    CANCELLED
  }

  enum PaymentStatus {
    PENDING
    AUTHORIZED
    CAPTURED
    FAILED
    REFUNDED
  }
`;

const resolvers = {
  User: {
    orders: (user, _, { dataSources }) => {
      return dataSources.ordersAPI.getOrdersByUserId(user.id);
    }
  },
  Order: {
    __resolveReference(order, { dataSources }) {
      return dataSources.ordersAPI.getOrderById(order.id);
    },
    user: (order) => {
      return { __typename: 'User', id: order.userId };
    }
  },
  OrderItem: {
    product: (orderItem) => {
      return { __typename: 'Product', id: orderItem.productId };
    },
    subtotal: (orderItem) => {
      return orderItem.price * orderItem.quantity;
    }
  }
};

๐Ÿ’ป Implementation Details

Key implementation patterns for building a robust GraphQL gateway with microservices.

DataLoader for Batching

// shared/dataloaders.js
const DataLoader = require('dataloader');

class ServiceDataLoaders {
  constructor(services) {
    this.services = services;
    
    // User DataLoader
    this.userLoader = new DataLoader(async (userIds) => {
      const users = await this.services.users.getUsersByIds(userIds);
      return userIds.map(id => users.find(u => u.id === id) || null);
    }, { cache: true, maxBatchSize: 100 });

    // Product DataLoader
    this.productLoader = new DataLoader(async (productIds) => {
      const products = await this.services.products.getProductsByIds(productIds);
      return productIds.map(id => products.find(p => p.id === id) || null);
    }, { cache: true, maxBatchSize: 100 });

    // Order DataLoader
    this.orderLoader = new DataLoader(async (orderIds) => {
      const orders = await this.services.orders.getOrdersByIds(orderIds);
      return orderIds.map(id => orders.find(o => o.id === id) || null);
    }, { cache: true, maxBatchSize: 100 });

    // Review DataLoader by Product
    this.reviewsByProductLoader = new DataLoader(async (productIds) => {
      const reviewsMap = await this.services.reviews.getReviewsByProductIds(productIds);
      return productIds.map(id => reviewsMap[id] || []);
    }, { cache: true, maxBatchSize: 50 });
  }

  // Clear all caches
  clearAll() {
    this.userLoader.clearAll();
    this.productLoader.clearAll();
    this.orderLoader.clearAll();
    this.reviewsByProductLoader.clearAll();
  }
}

module.exports = ServiceDataLoaders;

Service Communication

// shared/service-client.js
const axios = require('axios');
const CircuitBreaker = require('opossum');

class ServiceClient {
  constructor(serviceName, baseURL, options = {}) {
    this.serviceName = serviceName;
    this.baseURL = baseURL;
    
    // Create axios instance
    this.client = axios.create({
      baseURL,
      timeout: options.timeout || 5000,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    // Circuit breaker configuration
    const breakerOptions = {
      timeout: 3000,
      errorThresholdPercentage: 50,
      resetTimeout: 30000,
      rollingCountTimeout: 10000,
      rollingCountBuckets: 10
    };

    // Wrap requests in circuit breaker
    this.breaker = new CircuitBreaker(
      async (config) => this.client.request(config),
      breakerOptions
    );

    // Circuit breaker events
    this.breaker.on('open', () => {
      console.error(`Circuit breaker OPEN for ${serviceName}`);
    });

    this.breaker.on('halfOpen', () => {
      console.warn(`Circuit breaker HALF-OPEN for ${serviceName}`);
    });

    this.breaker.on('close', () => {
      console.info(`Circuit breaker CLOSED for ${serviceName}`);
    });
  }

  async query(query, variables = {}, context = {}) {
    try {
      const response = await this.breaker.fire({
        method: 'POST',
        url: '/graphql',
        data: { query, variables },
        headers: {
          'x-user-id': context.userId,
          'x-request-id': context.requestId,
          ...context.headers
        }
      });

      if (response.data.errors) {
        throw new ServiceError(
          `GraphQL errors from ${this.serviceName}`,
          response.data.errors
        );
      }

      return response.data.data;
    } catch (error) {
      if (error.code === 'EOPENBREAKER') {
        throw new ServiceUnavailableError(
          `Service ${this.serviceName} is currently unavailable`
        );
      }
      throw error;
    }
  }

  async rest(method, endpoint, data = null, context = {}) {
    try {
      const response = await this.breaker.fire({
        method,
        url: endpoint,
        data,
        headers: {
          'x-user-id': context.userId,
          'x-request-id': context.requestId,
          ...context.headers
        }
      });

      return response.data;
    } catch (error) {
      throw new ServiceError(
        `REST error from ${this.serviceName}`,
        error
      );
    }
  }

  getHealth() {
    return {
      service: this.serviceName,
      circuitBreakerOpen: this.breaker.opened,
      stats: this.breaker.stats
    };
  }
}

class ServiceError extends Error {
  constructor(message, originalError) {
    super(message);
    this.name = 'ServiceError';
    this.originalError = originalError;
  }
}

class ServiceUnavailableError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ServiceUnavailableError';
  }
}

module.exports = { ServiceClient, ServiceError, ServiceUnavailableError };

Authentication & Authorization

// gateway/middleware/auth.js
const jwt = require('jsonwebtoken');
const { AuthenticationError, ForbiddenError } = require('apollo-server');

const JWT_SECRET = process.env.JWT_SECRET;

// Verify JWT token
const verifyToken = (token) => {
  try {
    return jwt.verify(token, JWT_SECRET);
  } catch (error) {
    throw new AuthenticationError('Invalid or expired token');
  }
};

// Extract token from request
const getToken = (req) => {
  const authHeader = req.headers.authorization || '';
  const token = authHeader.replace('Bearer ', '');
  return token || null;
};

// Authentication directive
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    
    field.resolve = async function (...args) {
      const context = args[2];
      
      if (!context.user) {
        throw new AuthenticationError('Authentication required');
      }
      
      return resolve.apply(this, args);
    };
  }
}

// Role-based authorization directive
class HasRoleDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const { role: requiredRole } = this.args;
    
    field.resolve = async function (...args) {
      const context = args[2];
      
      if (!context.user) {
        throw new AuthenticationError('Authentication required');
      }
      
      if (context.user.role !== requiredRole) {
        throw new ForbiddenError(
          `Requires ${requiredRole} role`
        );
      }
      
      return resolve.apply(this, args);
    };
  }
}

// Context builder for Apollo Server
const buildContext = async ({ req }) => {
  const token = getToken(req);
  let user = null;
  
  if (token) {
    try {
      user = verifyToken(token);
    } catch (error) {
      console.error('Token verification failed:', error.message);
    }
  }
  
  return {
    user,
    userId: user?.id,
    requestId: req.headers['x-request-id'] || generateRequestId(),
    headers: req.headers
  };
};

// Usage in schema
const typeDefs = gql`
  directive @auth on FIELD_DEFINITION
  directive @hasRole(role: String!) on FIELD_DEFINITION

  type Query {
    me: User @auth
    users: [User!]! @hasRole(role: "ADMIN")
  }

  type Mutation {
    updateProfile(input: UpdateProfileInput!): User! @auth
    deleteUser(id: ID!): Boolean! @hasRole(role: "ADMIN")
  }
`;

module.exports = {
  verifyToken,
  getToken,
  AuthDirective,
  HasRoleDirective,
  buildContext
};

๐Ÿ”— Apollo Federation

Apollo Federation enables a distributed graph architecture where multiple subgraphs contribute types and fields to a unified supergraph.

Schema Stitching vs Apollo Federation

Feature Schema Stitching Apollo Federation
Type Ownership Single service owns each type Multiple services can extend types
Schema Composition Gateway merges schemas at runtime Declarative with @key directives
Type Resolution Manual delegation configuration Automatic via __resolveReference
Performance Additional network overhead Optimized query planning
Learning Curve Moderate - custom resolvers Steeper - federation concepts
Tooling graphql-tools ecosystem Apollo Platform (Studio, Router)
Best For Legacy system integration New microservices architecture

Federation Directives

// Federation v2 Directives

// @key - Defines entity with unique identifier
type User @key(fields: "id") {
  id: ID!
  email: String!
}

// @extends - Extends entity from another service (v1 only)
extend type User @key(fields: "id") {
  orders: [Order!]!
}

// @external - Marks field owned by another service
extend type Product @key(fields: "id") {
  id: ID! @external
  name: String! @external
  reviews: [Review!]!
}

// @requires - Field depends on other fields
type Product @key(fields: "id") {
  id: ID!
  price: Float!
  tax: Float! @requires(fields: "price")
}

// @provides - Optimization hint for nested fields
type Order {
  product: Product! @provides(fields: "name price")
}

// @shareable - Multiple subgraphs can define same field (v2)
type Product @key(fields: "id") {
  id: ID!
  name: String! @shareable
}

// @override - Replace field implementation from another subgraph (v2)
type User @key(fields: "id") {
  id: ID!
  email: String! @override(from: "legacy-users")
}

// @inaccessible - Hide field from supergraph (v2)
type User {
  id: ID!
  internalId: String! @inaccessible
}

// Composite @key for multi-field keys
type OrderItem @key(fields: "orderId productId") {
  orderId: ID!
  productId: ID!
  quantity: Int!
}

Query Planning

# Client Query
query GetUserWithOrders {
  user(id: "123") {
    id
    username
    email
    orders {
      id
      total
      items {
        product {
          name
          price
        }
        quantity
      }
    }
  }
}

# Query Plan Generated by Gateway
{
  "kind": "QueryPlan",
  "node": {
    "kind": "Sequence",
    "nodes": [
      {
        "kind": "Fetch",
        "serviceName": "users",
        "query": "{ user(id: \"123\") { id username email } }"
      },
      {
        "kind": "Parallel",
        "nodes": [
          {
            "kind": "Fetch",
            "serviceName": "orders",
            "requires": [{ "__typename": "User", "id": "123" }],
            "query": "{ _entities(representations: [...]) { ... on User { orders { id total items { productId quantity } } } } }"
          }
        ]
      },
      {
        "kind": "Fetch",
        "serviceName": "products",
        "requires": [{ "__typename": "Product", "id": [...] }],
        "query": "{ _entities(representations: [...]) { ... on Product { name price } } }"
      }
    ]
  }
}

# Execution Flow:
# 1. Fetch user from users-service
# 2. Fetch orders from orders-service (parallel)
# 3. Fetch product details from products-service
# 4. Merge results and return to client

โšก Performance Optimization

Strategies for optimizing gateway performance, reducing latency, and improving scalability.

๐Ÿ—ƒ๏ธ

Response Caching

Redis-based caching with TTL strategies for frequently accessed data, reducing load on microservices.

๐Ÿ“ฆ

Query Batching

DataLoader batches multiple requests into single database queries, eliminating N+1 problems.

๐ŸŽฏ

Query Complexity

Limits on query depth and complexity prevent expensive operations from overwhelming services.

Redis Caching Layer

// gateway/cache/redis-cache.js
const Redis = require('ioredis');
const { createHash } = require('crypto');

class RedisCache {
  constructor() {
    this.client = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD,
      retryStrategy: (times) => Math.min(times * 50, 2000)
    });

    this.defaultTTL = 300; // 5 minutes
  }

  // Generate cache key from query
  generateKey(query, variables = {}) {
    const hash = createHash('sha256');
    hash.update(JSON.stringify({ query, variables }));
    return `gql:${hash.digest('hex')}`;
  }

  // Get cached result
  async get(query, variables) {
    try {
      const key = this.generateKey(query, variables);
      const cached = await this.client.get(key);
      
      if (cached) {
        console.log(`Cache HIT: ${key}`);
        return JSON.parse(cached);
      }
      
      console.log(`Cache MISS: ${key}`);
      return null;
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  }

  // Set cache with TTL
  async set(query, variables, data, ttl = this.defaultTTL) {
    try {
      const key = this.generateKey(query, variables);
      await this.client.setex(
        key,
        ttl,
        JSON.stringify(data)
      );
      console.log(`Cache SET: ${key} (TTL: ${ttl}s)`);
    } catch (error) {
      console.error('Cache set error:', error);
    }
  }

  // Invalidate cache by pattern
  async invalidate(pattern) {
    try {
      const keys = await this.client.keys(`gql:${pattern}*`);
      if (keys.length > 0) {
        await this.client.del(...keys);
        console.log(`Cache INVALIDATE: ${keys.length} keys`);
      }
    } catch (error) {
      console.error('Cache invalidate error:', error);
    }
  }

  // Get cache statistics
  async getStats() {
    const info = await this.client.info('stats');
    return {
      hits: this.parseInfo(info, 'keyspace_hits'),
      misses: this.parseInfo(info, 'keyspace_misses'),
      keys: await this.client.dbsize()
    };
  }

  parseInfo(info, key) {
    const match = info.match(new RegExp(`${key}:(\\d+)`));
    return match ? parseInt(match[1]) : 0;
  }
}

// Apollo Server cache plugin
const responseCachePlugin = (cache) => ({
  async requestDidStart() {
    return {
      async willSendResponse({ request, response, context }) {
        // Only cache successful queries
        if (
          request.operationName &&
          !response.errors &&
          request.http?.method === 'GET'
        ) {
          await cache.set(
            request.query,
            request.variables,
            response.data,
            300 // 5 minutes
          );
        }
      }
    };
  }
});

module.exports = { RedisCache, responseCachePlugin };

Query Complexity Limiting

// gateway/plugins/complexity.js
const { createComplexityLimitRule } = require('graphql-validation-complexity');

const complexityLimitPlugin = {
  requestDidStart() {
    return {
      didResolveOperation({ request, document, schema }) {
        // Calculate query complexity
        const complexity = getComplexity({
          schema,
          query: document,
          variables: request.variables,
          estimators: [
            // Fields have base cost of 1
            simpleEstimator({ defaultComplexity: 1 }),
            
            // List fields multiply by limit/first argument
            fieldExtensionsEstimator(),
            
            // Custom complexity for expensive fields
            directiveEstimator({
              name: 'complexity',
              argumentName: 'value'
            })
          ]
        });

        const maxComplexity = 1000;
        
        if (complexity > maxComplexity) {
          throw new Error(
            `Query is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}`
          );
        }

        console.log(`Query complexity: ${complexity}/${maxComplexity}`);
      }
    };
  }
};

// Schema with complexity annotations
const typeDefs = gql`
  type Query {
    # Simple query - complexity: 1
    user(id: ID!): User
    
    # List with multiplier - complexity: limit * 1
    users(limit: Int = 10): [User!]!
    
    # Expensive operation - complexity: 50
    searchProducts(query: String!): [Product!]! @complexity(value: 50)
  }

  type User {
    id: ID!
    email: String!
    
    # Nested list - complexity: limit * 5
    orders(limit: Int = 10): [Order!]! @complexity(value: 5)
  }
`;

// Example complexity calculations:
// Query 1: user(id: "123") { email } = 1
// Query 2: users(limit: 100) { email } = 100
// Query 3: users(limit: 10) { orders(limit: 20) { id } } = 10 + (10 * 20 * 5) = 1010 (REJECTED!)

module.exports = complexityLimitPlugin;

๐Ÿš€ Deployment & DevOps

Production-ready deployment with Docker, Kubernetes, and CI/CD pipelines.

Docker Compose Setup

# docker-compose.yml
version: '3.8'

services:
  # Gateway
  gateway:
    build:
      context: ./gateway
      dockerfile: Dockerfile
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
      - JWT_SECRET=${JWT_SECRET}
      - REDIS_HOST=redis
      - USERS_SERVICE_URL=http://users-service:4001/graphql
      - PRODUCTS_SERVICE_URL=http://products-service:4002/graphql
      - ORDERS_SERVICE_URL=http://orders-service:4003/graphql
      - REVIEWS_SERVICE_URL=http://reviews-service:4004/graphql
    depends_on:
      - redis
      - users-service
      - products-service
      - orders-service
      - reviews-service
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:4000/.well-known/apollo/server-health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Users Service
  users-service:
    build:
      context: ./services/users
      dockerfile: Dockerfile
    ports:
      - "4001:4001"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/users_db
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis

  # Products Service
  products-service:
    build:
      context: ./services/products
      dockerfile: Dockerfile
    ports:
      - "4002:4002"
    environment:
      - NODE_ENV=production
      - MONGODB_URI=mongodb://mongo:27017/products_db
      - REDIS_HOST=redis
    depends_on:
      - mongo
      - redis

  # Orders Service
  orders-service:
    build:
      context: ./services/orders
      dockerfile: Dockerfile
    ports:
      - "4003:4003"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/orders_db
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis

  # Reviews Service
  reviews-service:
    build:
      context: ./services/reviews
      dockerfile: Dockerfile
    ports:
      - "4004:4004"
    environment:
      - NODE_ENV=production
      - MONGODB_URI=mongodb://mongo:27017/reviews_db
      - REDIS_HOST=redis
    depends_on:
      - mongo
      - redis

  # PostgreSQL
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-postgres.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"

  # MongoDB
  mongo:
    image: mongo:7
    environment:
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=password
    volumes:
      - mongo_data:/data/db
    ports:
      - "27017:27017"

  # Redis
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"

  # Monitoring - Prometheus
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"

  # Monitoring - Grafana
  grafana:
    image: grafana/grafana:latest
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana-dashboards:/etc/grafana/provisioning/dashboards
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

volumes:
  postgres_data:
  mongo_data:
  redis_data:
  prometheus_data:
  grafana_data:

Kubernetes Deployment

# k8s/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: graphql-gateway
  labels:
    app: graphql-gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: graphql-gateway
  template:
    metadata:
      labels:
        app: graphql-gateway
    spec:
      containers:
      - name: gateway
        image: your-registry/graphql-gateway:latest
        ports:
        - containerPort: 4000
          name: http
        env:
        - name: NODE_ENV
          value: "production"
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: gateway-secrets
              key: jwt-secret
        - name: REDIS_HOST
          value: "redis-service"
        - name: USERS_SERVICE_URL
          value: "http://users-service:4001/graphql"
        - name: PRODUCTS_SERVICE_URL
          value: "http://products-service:4002/graphql"
        - name: ORDERS_SERVICE_URL
          value: "http://orders-service:4003/graphql"
        - name: REVIEWS_SERVICE_URL
          value: "http://reviews-service:4004/graphql"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /.well-known/apollo/server-health
            port: 4000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /.well-known/apollo/server-health
            port: 4000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: graphql-gateway
spec:
  type: LoadBalancer
  selector:
    app: graphql-gateway
  ports:
  - protocol: TCP
    port: 80
    targetPort: 4000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: gateway-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: graphql-gateway
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Key Learnings

๐ŸŽฏ Service Boundaries

Clear service boundaries and well-defined schemas are crucial for maintainable microservices. Each service should own its domain completely.

๐Ÿ”„ Error Handling

Implement circuit breakers and graceful degradation. A failing microservice shouldn't bring down the entire gateway.

๐Ÿ“Š Observability

Distributed tracing is essential. Without proper monitoring, debugging issues across services becomes nearly impossible.

โšก Performance

DataLoader and caching are not optional - they're required for production. N+1 queries can kill performance at scale.

๐Ÿ” Security

Authentication at the gateway, authorization at the service level. Each service should validate permissions independently.

๐Ÿ“ Documentation

Self-documenting GraphQL schemas are powerful, but add descriptions and examples. Good documentation reduces support burden.

๐ŸŽ“ Conclusion

This GraphQL Gateway demonstrates a production-ready microservices architecture using Apollo Federation. The federated approach provides flexibility, scalability, and maintainability for complex distributed systems.

Key benefits include unified API, independent service deployment, type safety across services, and optimized query execution. The architecture supports both schema stitching and Apollo Federation, allowing teams to choose the best approach for their needs.

Apollo Server Apollo Federation Node.js PostgreSQL MongoDB Redis Docker Kubernetes DataLoader Circuit Breaker