Patron DataLoader en GraphQL
Consolida llamadas de carga individuales en llamadas batch con cache por peticion para prevenir queries N+1 y fetches redundantes.
Descripcion general
DataLoader es una utilidad generica que consolida llamadas individuales de load() en una sola peticion batch. Fue construido en Facebook para resolver el problema N+1 en servidores GraphQL. Dentro de un tick del event loop, todas las llamadas a load(key) se recolectan, se deduplican, y se pasan a una funcion batch que obtiene todas las claves a la vez. Los resultados se cachean por la vida util de la instancia del loader.
El patron no es especifico de GraphQL — funciona en cualquier lugar donde necesites batchear lookups async individuales. Pero brilla en GraphQL donde los resolvers anidados piden datos relacionados de forma independiente.
Cuando Usar
-
For alternatives, see GraphQL Batched Resolver Pattern.
-
Resolvers GraphQL que obtienen entidades relacionadas por item padre
-
Cualquier patron de lookup async donde llamadas individuales pueden batchearse (base de datos, REST, gRPC)
-
Prevenir fetches duplicados cuando la misma clave se pide multiples veces en una operacion
-
Capas de gateway o BFF que agregan datos de multiples servicios backend
Cuando No Usar
- Lookups de un solo item sin oportunidad de batching (N=1)
- Acceso a datos sincrono (DataLoader es solo async)
- Caches de larga duracion (DataLoader cachea por peticion, no entre peticiones)
- Subscripciones en tiempo real donde el batching anade latencia
Solucion
1. Configuracion Basica de DataLoader
import DataLoader from 'dataloader';
type User = { id: string; name: string; email: string };
async function batchUsers(ids: readonly string[]): Promise<User[]> {
// Una sola query para todos los IDs pedidos
const rows = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids]);
const map = new Map(rows.map((r) => [r.id, r]));
// Debe retornar en el mismo orden que los ids de entrada
return ids.map((id) => map.get(id) ?? new Error(`User ${id} not found`));
}
const userLoader = new DataLoader(batchUsers);
2. Usar en Resolvers GraphQL
const resolvers = {
Query: {
posts: (_parent, _args, { db }) => db.post.findMany({ take: 20 }),
},
Post: {
author: (post, _args, { loaders }) => loaders.userLoader.load(post.authorId),
comments: (post, _args, { loaders }) =>
loaders.commentLoader.loadMany(post.commentIds),
},
Comment: {
author: (comment, _args, { loaders }) => loaders.userLoader.load(comment.authorId),
},
};
Cuando una query pide 20 posts con sus autores y comentarios con sus autores, DataLoader batchea:
- Una llamada a
batchUserscon todos los IDs de autor unicos de posts y comentarios - Una llamada a
batchCommentscon todos los arrays de IDs de comentarios
3. Contexto por Peticion
Crear loaders frescos para cada operacion GraphQL:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
function createLoaders(db) {
return {
userLoader: new DataLoader((ids) => batchUsers(ids, db)),
commentLoader: new DataLoader((ids) => batchComments(ids, db)),
productLoader: new DataLoader((ids) => batchProducts(ids, db)),
};
}
const server = new ApolloServer({ typeDefs, resolvers });
startStandaloneServer(server, {
context: async ({ req }) => {
const db = getDb();
return {
db,
loaders: createLoaders(db),
user: parseAuth(req.headers.authorization),
};
},
listen: { port: 4000 },
});
4. Caching y Deduplicacion
// Si dos posts tienen el mismo authorId, el usuario se obtiene una vez
const posts = [
{ id: 1, authorId: 'user-10' },
{ id: 2, authorId: 'user-10' }, // mismo usuario
{ id: 3, authorId: 'user-20' },
];
// Cada resolver llama loaders.userLoader.load(post.authorId)
// DataLoader recolecta: ['user-10', 'user-10', 'user-20']
// Deduplica a: ['user-10', 'user-20']
// Una llamada batch, retorna resultado cacheado para el segundo 'user-10'
5. Primear el Cache
Cuando ya tienes datos, primea el loader para evitar fetches redundantes:
const resolvers = {
Query: {
user: async (_parent, { id }, { db, loaders }) => {
const user = await db.user.findById(id);
// Primear el cache — resolvers anidados que llamen load(id) obtienen esto al instante
loaders.userLoader.prime(id, user);
return user;
},
},
};
6. Limpiar Cache Despues de Mutaciones
const resolvers = {
Mutation: {
updateUser: async (_parent, { id, input }, { db, loaders }) => {
const user = await db.user.update(id, input);
// Limpiar entrada stale del cache — el proximo load() re-fetcheara
loaders.userLoader.clear(id);
return user;
},
},
};
Explicacion
- Batching: Todas las llamadas
load()dentro de un tick del event loop se recolectan. - Deduplicacion: Claves duplicadas en el mismo batch se piden una vez.
- Cache por peticion: El cache es un
Mapen la instancia del loader. - Contrato de orden: La funcion batch debe retornar un array de la misma longitud que la entrada, en el mismo orden.
- Manejo de errores: Retornar un objeto
Erroren un indice especifico para rechazar solo ese caller, no todo el batch
Variantes
DataLoader con Prisma
const userLoader = new DataLoader(async (ids: readonly string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: [...ids] } },
});
const map = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => map.get(id));
});
DataLoader con Pipeline de Redis
const sessionLoader = new DataLoader(async (sessionIds: readonly string[]) => {
const pipeline = redis.pipeline();
sessionIds.forEach((id) => pipeline.get(`session:${id}`));
const results = await pipeline.exec();
return results.map(([err, value]) => {
if (err) return new Error(err.message);
return value ? JSON.parse(value) : null;
});
});
Cache Key Function Personalizado
Para claves compuestas o no-string:
const permissionLoader = new DataLoader(
async (keys: readonly { userId: string; resource: string }[]) => {
const permissions = await db.permission.findMany({
where: { OR: keys.map((k) => ({ userId: k.userId, resource: k.resource })) },
});
const map = new Map(
permissions.map((p) => [`${p.userId}:${p.resource}`, p])
);
return keys.map((k) => map.get(`${k.userId}:${k.resource}`));
},
{
cacheKeyFn: (key) => `${key.userId}:${key.resource}`,
}
);
Tamano Maximo de Batch
Limitar el tamano del batch para restricciones de queries de base de datos:
const userLoader = new DataLoader(batchUsers, {
maxBatchSize: 50, // Max 50 IDs por query SELECT ... IN (...)
batchScheduleFn: (callback) => setTimeout(callback, 10), // ventana de 10ms
});
Deshabilitar Cache para Datos Mutables
const stockPriceLoader = new DataLoader(batchStockPrices, {
cache: false, // Los precios de acciones cambian constantemente — no cachear
});
Mejores Practicas
- Crear una nueva instancia de DataLoader por peticion — nunca compartir entre peticiones
- Retornar resultados en el orden exacto de las claves de entrada en funciones batch
- Usar
prime()cuando los datos ya estan disponibles desde un resolver padre - Llamar
clear(id)despues de mutaciones que modifican entidades cacheadas - Configurar
maxBatchSizepara mantener clausulasINdentro de los limites de la base de datos - Retornar objetos
Errorpor clave en lugar de lanzar en funciones batch - Usar
cacheKeyFnpara claves no-string o compuestas - Nombrar loaders segun la entidad que cargan (
userLoader, nodataLoader)
Errores Comunes
- Compartir un DataLoader entre peticiones: El cache entre peticiones filtra datos entre usuarios y causa lecturas stale
- Lanzar en funcion batch: Rechaza todo el batch.
- Orden de resultados incorrecto: DataLoader empareja por posicion.
- No limpiar despues de mutaciones: Datos cacheados stale se retornan para entidades actualizadas
- Usar DataLoader para caching de larga duracion: El cache de DataLoader es por peticion.
- Falta
cacheKeyFnpara claves objeto:{ userId: '1' }y{ userId: '1' }son referencias de objeto diferentes — cache miss cada vez
Troubleshooting
- Query returns null unexpectedly: verify resolvers, data loaders, and authorization.
- N+1 query performance issue: Inspect resolver execution traces.
- Introspection disabled in production breaks tools: enable it only in development, or use schema artifacts in CI.
- Mutation input rejected: confirm input validation, custom scalars, and whether variables are passed as the right type.
- Subscription stops receiving events: check the pub/sub backend, event filtering, and that the resolver is emitting events.
Puntos Clave
- Aplica patron dataloader en graphql cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.
Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patron de Resolver Batch en GraphQL
Resuelve campos anidados de GraphQL en una sola peticion batch para eliminar consultas N+1 y reducir la carga de la base de datos.
GuideGuía Completa de GraphQL Federation
Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.
GuideGuía Completa de GraphQL Federation
Construye APIs GraphQL unificadas con Apollo Federation. Cubre subgraphs, supergraph, resolución de entidades y despliegue de gateway.
RecipeDetectar y corregir consultas N+1 en resolvers GraphQL
Identifica problemas de consultas N+1 en resolvers GraphQL usando logging, DataLoader y herramientas de analisis antes de que lleguen a produccion
RecipeBatch y cache de consultas con GraphQL DataLoader
Usa DataLoader para agrupar peticiones de carga individuales en llamadas batch a la base de datos, resolviendo el problema N+1 en resolvers GraphQL
PatternPatron GraphQL Connection Pagination
Implementa paginacion basada en cursores estilo Relay con edges, nodes y pageInfo para queries de listas estables en GraphQL.