Skip to content
All essays
CraftMarch 27, 202415 min

Advanced GraphQL Federation Patterns

Master advanced patterns for building scalable federated GraphQL architectures.

Ü
Ümit Uz
Mobile & Full Stack Developer

Take your GraphQL Federation to the next level with advanced patterns for complex use cases.

Distributed Tracing

Trace ID Propagation

javascript
const { ApolloServer } = require('@apollo/server');

const server = new ApolloServer({
  context: async ({ req }) => {
    const traceId = req.headers['x-trace-id'] || generateTraceId();

    return {
      traceId,
      // Propagate to downstream services
      fetchOptions: {
        headers: {
          'X-Trace-ID': traceId,
        },
      },
    };
  },
});

// In resolvers
const resolvers = {
  Query: {
    user: async (_, { id }, { traceId, dataSources }) => {
      const user = await dataSources.usersAPI.getUser(id, {
        headers: { 'X-Trace-ID': traceId },
      });

      return user;
    },
  },
};

Authentication in Federation

Gateway Authentication

javascript
// Gateway
const { ApolloGateway } = require('@apollo/gateway');

const gateway = new ApolloGateway({
  async context({ req }) {
    const token = req.headers.authorization || '';

    const user = await authenticateToken(token);

    return {
      user,
      // Pass user to subgraphs
      headers: {
        'X-User-Id': user.id,
        'X-User-Role': user.role,
      },
    };
  },
});

Subgraph Authentication

javascript
// Subgraph
const server = new ApolloServer({
  context: async ({ req }) => {
    const userId = req.headers['x-user-id'];

    if (!userId) {
      throw new AuthenticationError('Unauthorized');
    }

    return {
      userId,
      userRole: req.headers['x-user-role'],
    };
  },
});

Authorization

Field-Level Authorization

javascript
const resolvers = {
  User: {
    email: (user, _, { userRole }) => {
      // Only admins can see emails
      if (userRole !== 'admin') {
        return null;
      }
      return user.email;
    },
  },
};

Directive-Based Authorization

graphql
directive @auth(requires: Role) on FIELD_DEFINITION

enum Role {
  ADMIN
  USER
}

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

type Query {
  adminData: String @auth(requires: ADMIN)
}
javascript
const { defaultFieldResolver } = require('graphql');

const authDirective = (schema) => {
  return mapSchema(schema, {
    [MapperKind.FIELD]: (fieldConfig) => {
      const authDirectives = getDirectives(
        schema,
        fieldConfig
      ).auth;

      if (authDirectives) {
        const { requires } = authDirectives;
        const originalResolve = fieldConfig.resolve ||
          defaultFieldResolver;

        fieldConfig.resolve = function(
          source,
          args,
          context,
          info
        ) {
          const userRole = context.userRole;

          if (requires === 'ADMIN' && userRole !== 'admin') {
            throw new AuthenticationError('Unauthorized');
          }

          return originalResolve(source, args, context, info);
        };
      }
      return fieldConfig;
    },
  });
};

Custom Directives

@uppercase Directive

graphql
directive @uppercase on FIELD_DEFINITION

type User @key(fields: "id") {
  id: ID!
  name: String! @uppercase
  email: String!
}
javascript
const uppercaseDirective = (schema) => {
  return mapSchema(schema, {
    [MapperKind.FIELD]: (fieldConfig) => {
      const uppercaseDirectives = getDirectives(
        schema,
        fieldConfig
      ).uppercase;

      if (uppercaseDirectives) {
        const originalResolve = fieldConfig.resolve ||
          defaultFieldResolver;

        fieldConfig.resolve = async function(
          source,
          args,
          context,
          info
        ) {
          const result = await originalResolve(
            source,
            args,
            context,
            info
          );

          return typeof result === 'string'
            ? result.toUpperCase()
            : result;
        };
      }
      return fieldConfig;
    },
  });
};

Batch Loading

DataLoader Pattern

javascript
const DataLoader = require('dataloader');

class UsersDataSource {
  constructor() {
    this.userLoader = new DataLoader(async (userIds) => {
      const users = await this.batchGetUsers(userIds);

      return userIds.map(id =>
        users.find(u => u.id === id) || null
      );
    });
  }

  async getUser(id) {
    return this.userLoader.load(id);
  }

  async batchGetUsers(userIds) {
    return await db.users.where('id').in(userIds);
  }
}

const resolvers = {
  Post: {
    author: async (post, _, { dataSources }) => {
      return dataSources.users.getUser(post.authorId);
    },
  },
};

Cross-Subgraph Batching

javascript
// Gateway context
const gateway = new ApolloGateway({
  context: async () => {
    return {
      loaders: {
        user: new DataLoader(async (ids) => {
          return fetchUsersByIds(ids);
        }),
      },
    };
  },
});

// In subgraph
const resolvers = {
  Post: {
    author: (post, _, { loaders }) => {
      return loaders.user.load(post.authorId);
    },
  },
};

Caching Strategies

Response Caching

javascript
const responseCachePlugin = require('@apollo/server-plugin-response-cache');

const server = new ApolloServer({
  plugins: [
    responseCachePlugin({
      session: async (requestContext) => {
        // Cache per user
        return requestContext.request.http.headers.get(
          'authorization'
        );
      },
    }),
  ],
});

// Cache hint in schema
type Query {
  user(id: ID!): User @cacheControl(maxAge: 60)
}

Subgraph Caching

javascript
const { ApolloServer } = require('@apollo/server');

const server = new ApolloServer({
  cache: new InMemoryLRUCache(),
  // Or Redis
  cache: new RedisCache({
    host: 'redis',
    port: 6379,
  }),
});

Schema Stitching vs Federation

When to Use Each

text
Schema Stitching:
- Single monolithic gateway
- Gateway transforms and combines schemas
- More control but more complexity

Federation:
- Declarative composition
- Subgraphs own their schemas
- Better for large teams
- Easier to maintain

Migrating to Federation

Incremental Migration

javascript
// Step 1: Keep monolith, add federated services
const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'monolith', url: 'http://monolith:4000' },
      { name: 'users', url: 'http://users:4001' },
    ],
  }),
});

// Step 2: Move domains to subgraphs incrementally
// Step 3: Eventually decommission monolith

Monitoring and Observability

Custom Metrics

javascript
const { ApolloServer } = require('@apollo/server');

const server = new ApolloServer({
  plugins: [
    {
      requestDidStart: () => ({
        willSendResponse: ({ response }) => {
          const traceId = response.context.traceId;

          metrics.record('graphql.request', {
            traceId,
            operation: response.request.operationName,
            duration: response.body.kind === 'single'
              ? response.body.singleResult.extensions?.timing
              : 0,
          });
        },
      }),
    },
  ],
});

Performance Monitoring

javascript
const plugin = {
  requestDidStart: () => ({
    executionDidStart: () => ({
      willResolveField({ info }) {
        const start = Date.now();

        return () => {
          const duration = Date.now() - start;

          metrics.record('graphql.field.resolve', {
            fieldName: info.fieldName,
            typeName: info.parentType.name,
            duration,
          });
        };
      },
    }),
  }),
};

Best Practices

DOs

  • Use DataLoader for batching
  • Implement proper caching
  • Monitor performance
  • Handle errors gracefully
  • Use custom directives wisely
  • Test federated queries
  • Version subgraphs

DON'Ts

  • Create circular dependencies
  • Over-fetch across services
  • Ignore authentication
  • Skip error handling
  • Forget about monitoring
  • Mix concerns
  • Create tight coupling

Conclusion

Advanced GraphQL Federation patterns enable sophisticated architectures. Focus on performance with DataLoader, implement proper security, and monitor everything. Build scalable federated graphs that serve your teams and users well!

Enjoyed this essay? Share it.

Related essays

Next essay
Introduction to GraphQL Federation