Federation isn’t about merging your GraphQL services; it’s about composing them into a single, unified graph that clients can query as if it were one API.

Let’s say you have two GraphQL services: users-service and products-service.

users-service schema:

type User {
  id: ID!
  name: String!
  email: String
}

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

products-service schema:

type Product {
  id: ID!
  name: String!
  price: Float!
  ownerId: ID!
}

type Query {
  product(id: ID!): Product
}

A client might want to query for a product and its owner’s name. With federation, you can achieve this without products-service knowing anything about users-service.

Here’s how you’d set up a gateway service using Apollo Federation:

  1. Define the User type in products-service as an entity: In products-service, you need to tell the gateway that User is an entity it can reference.

    products-service/schema.graphql:

    extend type Query {
      product(id: ID!): Product
    }
    
    type Product @key(fields: "id") { # Mark Product as an entity
      id: ID!
      name: String!
      price: Float!
      ownerId: ID!
    }
    
    # Define User as a type that Product can reference, but don't implement its fields here
    type User @extends {
      id: ID! # Required to link
    }
    

    products-service/resolvers.js:

    // ... other resolvers
    Query: {
      product: (_, { id }) => { /* fetch product from DB */ },
    },
    Product: {
      // This resolver is only called when a query needs to resolve Product fields
      // *and* the gateway needs to fetch the full Product object by its ID.
      // It's *not* called when fetching a Product from a query that originated in products-service.
      __resolveReference(reference, { fetchUser }) {
        // The 'reference' object will contain the fields marked with @key.
        // In this case, it's { __typename: "Product", id: "..." }
        // You'd typically fetch the full product object here.
        // This resolver is not directly relevant for the User link, but shows how entities work.
      },
      // This resolver is crucial for linking!
      owner: (product, _, { dataSources }) => {
        // product here is the product object fetched by the gateway or from the product query.
        // We use ownerId to fetch the User.
        return dataSources.usersAPI.getUser(product.ownerId);
      }
    }
    
  2. Define the Product type in users-service as an entity: Similarly, users-service needs to know about Product and how to retrieve it by its key.

    users-service/schema.graphql:

    type Query {
      user(id: ID!): User
    }
    
    type User @key(fields: "id") { # Mark User as an entity
      id: ID!
      name: String!
      email: String
    }
    
    # Define Product as a type that User can reference
    type Product @extends {
      id: ID! # Required to link
    }
    

    users-service/resolvers.js:

    // ... other resolvers
    Query: {
      user: (_, { id }, { dataSources }) => dataSources.usersAPI.getUser(id),
    },
    User: {
      __resolveReference(reference, { fetchProduct }) {
        // The 'reference' object will contain { __typename: "User", id: "..." }
        // You'd typically fetch the full user object here.
      },
      // This resolver is crucial for linking!
      products: (user, _, { dataSources }) => {
        // user here is the user object fetched by the gateway or from the user query.
        // We use user.id to fetch products owned by this user.
        return dataSources.productsAPI.getProductsByOwner(user.id);
      }
    }
    
  3. Configure the Gateway: The gateway acts as the entry point and orchestrates requests to the underlying services.

    gateway/index.js:

    const { ApolloServer } = require('apollo-server');
    const { ApolloGateway } = require('apollo-gateway');
    
    const gateway = new ApolloGateway({
      serviceList: [
        { name: 'users', url: 'http://localhost:4001/graphql' },
        { name: 'products', url: 'http://localhost:4002/graphql' },
      ],
    });
    
    const server = new ApolloServer({
      gateway,
      // Subscriptions can also be configured here
    });
    
    server.listen(4000).then(({ url }) => {
      console.log(`🚀 Gateway ready at ${url}`);
    });
    

Now, a client can make a query like this against the gateway:

query GetProductOwner($productId: ID!) {
  product(id: $productId) {
    id
    name
    owner { # This field comes from the 'owner' resolver in products-service
      id
      name # This field comes from the 'name' field of the User type, resolved by users-service
      products { # This field comes from the 'products' resolver in users-service
        id
        name
      }
    }
  }
}

The gateway receives this query. It sees product and knows products-service can handle it. It queries products-service for the product. Then, it sees the owner field. Because Product has an owner field that returns a User, and User is an entity defined in users-service, the gateway knows to ask users-service for the User data. It passes the ownerId from the product to users-service for a User entity lookup. The process continues recursively for nested fields like products.

Caching with Federation:

Caching in a federated architecture is typically handled at the gateway level. Apollo Gateway supports automatic caching for entities. When a query requests an entity (e.g., User @key(fields: "id")), the gateway can cache the response based on the entity’s key (id). Subsequent requests for the same entity can be served directly from the cache, reducing load on downstream services.

You can configure a cache provider like apollo-server-caching for the gateway:

const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('apollo-gateway');
const { RESTDataSource } = require('apollo-datasource-rest');
const { InMemoryLRUCache } = require('apollo-server-caching'); // Or RedisCache, etc.

// ... serviceList definition ...

const gateway = new ApolloGateway({
  serviceList,
  buildService({ url }) {
    return new RemoteGraphQLDataSource({
      url,
      cache: new InMemoryLRUCache(), // Configure cache here
    });
  },
});

const server = new ApolloServer({
  gateway,
  // ... other ApolloServer config ...
});

This InMemoryLRUCache will store responses for entities, keyed by their __typename and their @key fields. For instance, a User with id: "123" might be cached under a key like User:123.

Authentication and Authorization:

Auth in a federated system is a layered concern:

  1. Gateway Authentication: The gateway can authenticate incoming requests. This typically involves validating a JWT or session token. If authentication fails, the request is rejected early. You’d implement this in the ApolloServer constructor for the gateway:

    const { ApolloServer } = require('apollo-server');
    const { ApolloGateway } = require('apollo-gateway');
    // ...
    
    const server = new ApolloServer({
      gateway,
      context: ({ req }) => {
        // Authenticate the request and attach user info to context
        const token = req.headers.authorization || '';
        const user = validateToken(token); // Your token validation logic
        return { user };
      },
    });
    
  2. Service Authorization: Once authenticated, the gateway forwards the request (and the user context) to the appropriate downstream service. Each service is then responsible for authorizing the specific operation based on the user’s identity and permissions.

    In the service’s resolvers, you’d access the user context:

    users-service/resolvers.js:

    Query: {
      user: (_, { id }, { user, dataSources }) => {
        if (!user || !user.isAdmin) { // Example authorization check
          throw new Error('Unauthorized');
        }
        return dataSources.usersAPI.getUser(id);
      },
    },
    

    Or, more granularly, within the User type’s fields:

    User: {
      email: (user, _, { user: currentUser }) => {
        if (user.id === currentUser.id || currentUser.isAdmin) {
          return user.email;
        }
        return null; // Mask email if not authorized
      },
      // ...
    }
    

    The key insight for authorization is that each service must be able to verify the identity and permissions of the user making the request. The gateway doesn’t magically authorize everything; it passes the authenticated identity, and services enforce their own boundaries. A common pattern is to ensure that the context object passed to each service’s resolvers contains the authenticated user’s details. This means the gateway needs to ensure that whatever authentication mechanism it uses injects this information into the context that is then passed down to the services.

The next challenge is handling complex data transformations and relationships across services when a single field requires data from multiple underlying services, or when you need to enforce fine-grained access control on specific fields across the entire graph.

Want structured learning?

Take the full Graphql-tools course →