GraphQL Federation: dividir un schema sin dividir equipos
Guía práctica de GraphQL Federation para dividir un schema monolítico entre servicios sin duplicar resolvers ni lógica de gateway.

Todo monolito de GraphQL llega a la misma pared. Un schema, un repo, un pipeline de deploy y, eventualmente, un equipo bloqueando el release de todos los demás porque su resolver tocó un tipo del que todos dependen. El instinto es dividir el schema como dividirías una API REST — por límite de servicio — pero los clientes de GraphQL esperan un solo grafo, no cinco. GraphQL Federation resuelve ese problema específico: varios equipos son dueños de subgraphs independientes, y un gateway los compone en un único supergraph consultable sin que nadie tenga que escribir a mano la lógica de stitching.
Esto no es un respaldo general a los microservicios. Es una herramienta específica para un problema específico: ya tienes límites de servicio definidos y necesitas que el grafo los refleje sin obligar a los clientes a saber que ese límite existe.
El problema de un schema único
Un servidor GraphQL monolítico centraliza la propiedad de cada tipo. User, Order, Product e Inventory viven en un mismo código base, resueltos por un mismo proceso. Eso funciona bien con diez tipos. Con doscientos, y cinco equipos haciendo commits al mismo schema.graphql, obtienes:
- Conflictos de merge en tipos compartidos que nadie posee claramente
- Un único punto de falla — un mal deploy de un resolver tumba todo el grafo
- Ninguna forma de escalar el servicio de
Inventoryde forma independiente de las validaciones de auth deUser
La solución ingenua — schema stitching — requería un proceso coordinador que fusionara manualmente los schemas remotos y reescribiera las queries. Funcionaba, pero cada subgraph nuevo necesitaba lógica de merge escrita a mano. Federation, tal como la especifica el spec de Federation de Apollo, reemplaza ese merge manual con un paso de composición declarativo: cada subgraph declara qué posee, y una herramienta de composición construye el supergraph automáticamente.
Cómo declara un subgraph su propiedad
Cada servicio expone un schema de GraphQL normal, anotado con directivas de federation. La clave es @key, que le indica al gateway cómo identificar una entidad a través de los subgraphs.
# subgraph: products
type Product @key(fields: "id") {
id: ID!
name: String!
priceCents: Int!
}
# subgraph: inventory
type Product @key(fields: "id") {
id: ID! @external
stockCount: Int!
warehouseId: ID!
}
extend type Query {
lowStockProducts(threshold: Int!): [Product!]!
}Dos servicios definen Product, pero ninguno posee el tipo completo. products posee name y priceCents; inventory posee stockCount. La directiva @key(fields: "id") le indica a la capa de composición que son la misma entidad, unida por id. Sin base de datos compartida, sin código base compartido — solo un identificador compartido.
Resolviendo referencias entre servicios
Cuando una query pide stockCount de un Product que se originó en el subgraph products, el gateway necesita que el subgraph inventory lo resuelva. Para eso está __resolveReference:
// inventory subgraph resolver
const resolvers = {
Product: {
async __resolveReference(reference: { id: string }) {
// ❌ Bad: N+1 query per product, one at a time
// return db.inventory.findOne({ productId: reference.id });
// ✅ Good: batch reference resolution with a dataloader
return inventoryLoader.load(reference.id);
},
},
Query: {
async lowStockProducts(_: unknown, args: { threshold: number }) {
const rows = await db.inventory.find({
stockCount: { $lt: args.threshold },
});
return rows.map((row) => ({ id: row.productId }));
},
},
};El gateway agrupa todas las llamadas a __resolveReference de un subgraph dado en una sola petición por query, así que esto se ejecuta una vez por query, no una vez por entidad — pero solo si también agrupas dentro del resolver. Si te saltas el dataloader, simplemente moviste el problema de N+1 del gateway al subgraph.
Por defecto, __resolveReference se llama una vez por entidad por query. Sin batching, una query que devuelve 50 productos dispara 50 round trips al subgraph de inventory, aunque federation ya agrupe a nivel de red.
Componiendo y sirviendo el supergraph
El gateway no fusiona schemas en runtime adivinando — consume un supergraph schema precalculado, producido por un paso de composición que valida que todos los subgraphs coincidan en los tipos compartidos.
// gateway.ts
import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway";
import { ApolloServer } from "@apollo/server";
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "products", url: "http://products.internal:4001/graphql" },
{ name: "inventory", url: "http://inventory.internal:4002/graphql" },
{ name: "orders", url: "http://orders.internal:4003/graphql" },
],
}),
});
const server = new ApolloServer({ gateway });IntrospectAndCompose es cómodo para desarrollo, pero recompone en cada reinicio del gateway contactando a los subgraphs en vivo — mala idea en producción, donde un subgraph brevemente inalcanzable no debería tumbar todo el grafo. Las configuraciones de producción, en cambio, publican un supergraph SDL ya compuesto en un registro de schemas y hacen que el gateway cargue un artefacto estático y pre-validado.
# supergraph-config.yaml — used by rover to compose ahead of deploy time
federation_version: 2
subgraphs:
products:
routing_url: https://products.internal/graphql
schema:
file: ./products/schema.graphql
inventory:
routing_url: https://inventory.internal/graphql
schema:
file: ./inventory/schema.graphqlLa composición ocurre en CI, contra archivos de schema, no contra tráfico en vivo. Un schema de subgraph roto hace fallar el build antes de que llegue al gateway. Es la misma disciplina que querrías para cualquier schema de GraphQL compartido entre equipos — validar el contrato antes de publicarlo, no después.
Migrando un monolito de forma incremental
No necesitas federar todo el primer día. El camino práctico refleja el patrón strangler fig: envuelve el monolito existente como un único subgraph y luego extrae los tipos de a uno.
# legacy-monolith subgraph — everything not yet extracted
extend type Query {
legacyUsers: [User!]!
}
type User @key(fields: "id") {
id: ID!
email: String!
# ownership will move to a dedicated `identity` subgraph later
}Al gateway no le importa que uno de los "subgraphs" sea en realidad todo tu viejo servidor. Los clientes ven un solo grafo en todo momento; la migración les resulta invisible. Ese es el valor real de federation frente a una reescritura — desacopla el problema del organigrama del problema del contrato de la API.
El costo de la planificación de queries
Cada query federada se planifica antes de ejecutarse: el gateway determina a qué subgraphs llamar, en qué orden, y cómo unir los resultados. Esa planificación tiene un costo real de latencia, que se acumula con el número de subgraphs.
| Subgraphs involucrados | Latencia adicional típica | Mitigación |
|---|---|---|
| 1–2 | insignificante | ninguna necesaria |
| 3–5 | 5–20ms | caché de query plans |
| 6+ | 20ms+ | reducir el fan-out de entidades, colocar juntos los paths críticos |
El caché de query plans (indexado por la query normalizada de la operación) elimina el costo de replanificación en queries repetidas, lo que cubre la mayor parte del tráfico en producción, ya que los clientes suelen enviar un conjunto pequeño y estable de operaciones.
Conclusiones clave
- Federation resuelve un problema organizacional — propiedad independiente de equipos — no un problema de rendimiento. No la uses solo para dividir archivos.
- Las directivas
@keydefinen la identidad de las entidades entre subgraphs; si te equivocas aquí, la composición falla o produce silenciosamente un join incorrecto. - Agrupa
__resolveReferencecon un dataloader — federation agrupa a nivel de red, no dentro de tu resolver. - Compón el supergraph en CI contra archivos de schema, no al arrancar el gateway contra tráfico en vivo.
- Migra de forma incremental envolviendo el monolito existente como un solo subgraph y extrayendo tipos con el tiempo.
- Cachea los query plans; replanificar operaciones idénticas en cada request es latencia desperdiciada que no necesitas pagar.


