StackPractices
advanced Por Mathias Paulenko

Patrón de entidades federadas en GraphQL

Compartí una entidad entre subgraphs de Apollo Federation. Usá @key, @external y @shareable para que cada servicio maneje los campos que conoce.

En Apollo Federation, una entidad es un object type al que varios subgraphs pueden contribuir al mismo tiempo. Un servicio posee los campos base, otros agregan los suyos y el gateway fusiona todo en un único tipo GraphQL.

Usé este patrón en producción con 8 subgraphs y es la forma más limpia de dividir un grafo entre equipos sin un esquema monolítico. La alternativa — schema stitching — funciona, pero mete demasiada lógica de glue en el gateway. Federation mantiene cada subgraph enfocado en los datos que ya maneja.

Este patrón mantiene cada subgraph enfocado en los datos que ya maneja — un servicio de usuarios posee User.name, un servicio de órdenes agrega User.orders y uno de reseñas agrega User.reviews. El cliente consulta un único esquema, pero el gateway direcciona cada campo al servicio correcto. Para un análisis más profundo, mirá la guía completa de GraphQL Federation.

Cuándo Usarlo

  • Diferentes microservicios poseen distintos campos del mismo objeto de dominio.
  • Querés una API GraphQL unificada sin construir un esquema monolítico.
  • El schema stitching se siente demasiado manual o propenso a errores.
  • Los equipos necesitan desplegar y ser dueños de su parte del grafo de forma independiente.

Cuándo NO Usarlo

  • La entidad solo se lee o escribe desde un servicio. Saltá federation — un esquema GraphQL común es más simple y evita el overhead.
  • Los subgraphs están fuertemente acoplados y comparten la misma base de datos. Federation agrega complejidad innecesaria.
  • Todavía estás en un servidor GraphQL que no soporta el spec de Apollo Federation.
  • Necesitás paginación basada en connections en campos federados — funciona, pero los query plans se vuelven costosos rápido.

Solución

Subgraph A: Servicio de Usuarios (posee la entidad)

El subgraph base declara la entidad con @key e implementa __resolveReference para que el gateway pueda obtenerla por su clave.

import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
  }
`;

const resolvers = {
  User: {
    __resolveReference(user: { id: string }) {
      return userService.getById(user.id);
    },
  },
  Query: {
    user(_: unknown, { id }: { id: string }) {
      return userService.getById(id);
    },
  },
};

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

const { url } = await startStandaloneServer(server, {
  listen: { port: 4001 },
});
console.log(`Servicio de usuarios listo en ${url}`);

Subgraph B: Servicio de Órdenes (extiende la entidad)

El subgraph de órdenes no posee User, pero le agrega User.orders. El campo id se marca como @external porque otro servicio lo posee. resolvable: false indica que este subgraph no puede traer un User por clave por sí solo.

const typeDefs = `#graphql
  type Order @key(fields: "id") {
    id: ID!
    userId: ID!
    total: Float!
  }

  type User @key(fields: "id", resolvable: false) {
    id: ID! @external
    orders: [Order!]!
  }
`;

const resolvers = {
  User: {
    orders(user: { id: string }) {
      return orderService.getByUserId(user.id);
    },
  },
  Query: {
    order(_: unknown, { id }: { id: string }) {
      return orderService.getById(id);
    },
  },
};

Subgraph C: Servicio de Reseñas (extiende la entidad nuevamente)

Otro subgraph también puede extender User. Cada uno declara solo los campos que conoce.

const typeDefs = `#graphql
  type Review @key(fields: "id") {
    id: ID!
    productId: ID!
    rating: Int!
    comment: String
  }

  type User @key(fields: "id", resolvable: false) {
    id: ID! @external
    reviews: [Review!]!
  }
`;

const resolvers = {
  User: {
    reviews(user: { id: string }) {
      return reviewService.getByUserId(user.id);
    },
  },
};

Gateway

El gateway consulta cada subgraph por su esquema, arma el supergraph y enruta campos automáticamente. Para una guía completa de setup con Docker, mirá la receta de federation gateway setup.

import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'orders', url: 'http://localhost:4002/graphql' },
      { name: 'reviews', url: 'http://localhost:4003/graphql' },
    ],
  }),
});

const server = new ApolloServer({ gateway });
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});
console.log(`Gateway listo en ${url}`);

Consulta del Cliente

El cliente ve un único tipo User, aunque los campos provengan de tres servicios.

query GetUserWithOrdersAndReviews {
  user(id: "123") {
    id
    name
    email
    orders {
      id
      total
    }
    reviews {
      id
      rating
      comment
    }
  }
}

Explicación

Federation resuelve esa consulta en tres pasos:

sequenceDiagram diagram: participant C as Cliente
  1. El gateway pide al subgraph de usuarios user(id: "123") y los campos base id, name, email.
  2. Envía el id a los subgraphs de órdenes y reseñas a través de la query _entities, pidiendo orders y reviews.
  3. Fusiona los resultados en un único objeto User y lo devuelve al cliente.

Las piezas clave son:

  • @key(fields: "id") — declara cómo el gateway identifica la entidad entre subgraphs. Pensalo como la clave primaria para el ruteo de federation.
  • @external — marca un campo que este subgraph usa pero no posee.
  • resolvable: false — señala que este subgraph no puede resolver la entidad por clave por sí solo; solo aporta campos cuando otro subgraph aporta la clave.
  • __resolveReference — el resolver que el gateway invoca para obtener una entidad por su clave. Lo implementás en el subgraph propietario; los subgraphs extensores no lo necesitan.

Federation 1 vs Federation 2

Apollo Federation 2 (lanzado en 2022) simplificó varias reglas. Los cambios más importantes desde Federation 1:

  • No se necesita @extends: En Federation 1, extender una entidad requería extend type User. Federation 2 lo infiere de @key + @external. El código de este patrón funciona con Federation 1 y 2.
  • @shareable es opt-in en Federation 1, default en Federation 2 para campos definidos en dos o más subgraphs. En Federation 2, marcás los campos no compartibles con @inaccessible.
  • Claves de un solo campo: Federation 2 permite que los subgraphs resuelvan entidades usando solo un subconjunto de los campos clave, lo cual no era posible en Federation 1.

Si estás arrancando un proyecto nuevo hoy, Federation 2 es el camino a seguir. La sintaxis es más limpia y el tooling (Rover, Apollo Studio) viene configurado por defecto.

Debuggear query plans de federation

El problema más común en producción con federation es el N+1 en __resolveReference. El gateway lo llama una vez por cada referencia de entidad, así que una query que devuelve 100 usuarios con orders dispara 100 llamadas separadas al subgraph de órdenes. Una vez debuggeé un query plan que hacía 47 llamadas a subgraphs porque alguien olvidó @external en el campo id — el gateway no podía batchear las referencias.

Para diagnosticar query plans:

  1. Apollo Studio — el query plan viewer muestra la secuencia exacta de llamadas a subgraphs. Buscá llamadas repetidas a _entities que se podrían batchear.
  2. rover supergraph compose — valida la composición del esquema localmente antes de deployear. Atrapa errores de @external / @key temprano.
  3. Logs del gateway — habilitá debug: true en la config del gateway para loguear query plans a stdout.
  4. Datadog / OpenTelemetry — traceá el span _entities para ver cuántas veces se llama cada subgraph por query.

Migrar campos con @override

Federation 2 introdujo @override para mover un campo de un subgraph a otro sin downtime:

type User @key(fields: "id") {
  id: ID!
  # El subgraph viejo todavía resuelve este campo
  displayName: String! @override(from: "users")
}

El subgraph nuevo toma la resolución gradualmente. Una vez que todo el tráfico usa el resolver nuevo, eliminá el campo del subgraph viejo. Usé esto para migrar un campo User.avatarUrl de un servicio legacy a un servicio nuevo de media sin un solo deploy window — el gateway rutea a cualquier subgraph que esté activo.

Variantes

VarianteDirectivaCaso de Uso
Clave simple@key(fields: "id")La mayoría de entidades con un ID primario
Clave compuesta@key(fields: "tenantId id")Entidades multi-tenant o particionadas
Múltiples claves@key(fields: "id") @key(fields: "email")Búsqueda por más de un identificador
Campo calculado@requires(fields: "address")Un campo que necesita datos de otro subgraph primero
Campo compartido@shareableUn campo resoluble por más de un subgraph

Ejemplo de clave compuesta

type User @key(fields: "tenantId id") {
  tenantId: ID!
  id: ID!
  name: String!
}

Ejemplo de @requires

El subgraph de envío calcula shippingCost a partir del address del usuario, que pertenece al subgraph de usuarios.

type User @key(fields: "id", resolvable: false) {
  id: ID! @external
  address: String! @external
  shippingCost: Float! @requires(fields: "address")
}

El gateway obtiene address primero y luego lo pasa al resolver de envío.

Buenas Prácticas

  • Mantené una sola definición base por entidad. Los otros subgraphs agregan campos, pero solo un servicio posee el tipo core.
  • Agrupá llamadas en __resolveReference. El gateway lo llama una vez por cada referencia de entidad, así que una query separada por entidad no escala — batchealas.
  • Marcá los campos clave como @external cuando el subgraph no los posea. Olvidar esto genera un error de validación de esquema.
  • Agregá nuevas claves antes de eliminar las viejas. Cambiar una clave en vuelo rompe los query plans existentes que los clientes puedan tener cacheados.
  • Usá Apollo Studio o el query plan viewer del gateway para verificar que los campos pasen por los subgraphs correctos.

Errores Comunes

  • Faltar __resolveReference en el subgraph propietario. Sin este resolver, el gateway no tiene forma de obtener la entidad por clave — las queries que tocan el tipo base van a fallar.
  • Olvidar @external en el campo id de una extensión. El gateway rechazará el esquema.
  • Declarar el tipo base User en dos subgraphs. Solo un subgraph debe poseer la definición base.
  • Devolver campos extra en __resolveReference. Solo devolvé lo que el resolver pidió; el gateway obtiene el resto de los otros subgraphs.
  • Referencias circulares profundas sin límites. User.orders.user.orders puede explotar los query plans — agregá límites de profundidad o paginación para prevenirlo.

Ver También

Preguntas frecuentes

¿En qué se diferencia federation de schema stitching?

Federation usa directivas y el protocolo _entities para fusionar esquemas de forma declarativa. Schema stitching escribe resolvers personalizados en el gateway para delegar campos manualmente. Federation es más limpio para servicios Apollo; stitching es más flexible para integrar APIs externas.

¿Puede un subgraph extender una entidad que no posee?

Sí. La declara con @key y marca el campo clave como @external. El gateway enruta los campos base al subgraph propietario y los nuevos al subgraph extensor.

¿Qué hace @requires?

Indica al gateway que debe obtener uno o más campos de otro subgraph antes de resolver el campo anotado. Usalo cuando un campo se calcula a partir de datos que pertenecen a otro lugar.

¿Puedo federar servicios REST?

No directamente. Cada subgraph debe exponer un esquema GraphQL que cumpla el spec de Apollo Federation. Si es necesario, podés envolver una API REST en un subgraph GraphQL liviano.

¿Debería marcar todos los campos como @shareable?

No. Usá @shareable solo cuando el mismo campo pueda resolverse por más de un subgraph. Para la mayoría de las extensiones, @key y @external alcanzan.