GraphQL virou um padrão de fato para muitas equipes que precisam entregar APIs flexíveis, especialmente em plataformas digitais com múltiplos clientes (web, mobile, parceiros). Mas, ao mesmo tempo em que o modelo de consulta do graphql melhora a experiência de consumo, ele também torna mais fácil cair em armadilhas de performance e arquitetura — com destaque para o N+1 Problem e para desafios de escala organizacional quando a API cresce.
Este artigo explica, de forma prática, três pilares para lidar com esse cenário:
- O que é o N+1 Problem em graphql e como identificá-lo
- Como o DataLoader ajuda com batching e caching por requisição
- O que é GraphQL Federation, quando faz sentido e quais cuidados de segurança observar
N+1 Problem em GraphQL: o que é e por que acontece
O N+1 Problem ocorre quando uma única consulta do cliente dispara:
- Uma query para buscar uma lista (o “1”)
- Mais N queries (ou chamadas) para buscar dados relacionados para cada item da lista (o “N”)
Em graphql, isso aparece com frequência por causa de resolvers encadeados. Um exemplo típico:
- O cliente pede
users { id name posts { id title } } - O resolver de
usersbusca a lista de usuários (1 consulta) - Para cada usuário, o resolver de
postsfaz uma consulta separada (N consultas)
Sintomas comuns
- A API “funciona” com poucos dados, mas degrada rapidamente em produção
- Picos de latência quando consultas pedem campos relacionais
- Aumento do consumo do banco (CPU/IO) e saturação de conexões
- Logs com muitas queries repetidas (por exemplo,
SELECT ... WHERE user_id = ?em loop)
Como confirmar o N+1 na prática
Um passo a passo comum para diagnosticar:
- Ative logs de queries no banco (ou no ORM) com correlação por request.
- Rastreie a requisição graphql com um
requestId. - Execute uma query que retorna muitos itens com um relacionamento.
- Conte quantas consultas ao banco acontecem:
- Se a contagem cresce linearmente com o tamanho da lista, é N+1.
Em ambientes com observabilidade, é útil medir:
- número de queries por requisição
- tempo total em banco
- p95/p99 de latência por operação graphql
DataLoader: batching e cache por requisição
O DataLoader é uma biblioteca popular (originalmente do ecossistema do Facebook) usada para mitigar N+1 por meio de duas estratégias:
- Batching: agrupar várias cargas do mesmo tipo em uma única consulta
- Caching: reutilizar resultados dentro da mesma requisição para evitar recomputação e chamadas duplicadas
A ideia central: em vez de cada resolver fazer uma consulta imediata, ele “agenda” uma carga. O DataLoader coleta várias dessas cargas durante o ciclo do event loop e executa uma operação em lote.
Como o batching resolve o N+1
Em vez de:
SELECT * FROM posts WHERE user_id = 1SELECT * FROM posts WHERE user_id = 2- …
SELECT * FROM posts WHERE user_id = N
Você faz:
SELECT * FROM posts WHERE user_id IN (1,2,...,N)
Depois, o DataLoader reorganiza os resultados para devolver ao resolver o array correspondente a cada chave.
Passo a passo de implementação (visão geral)
- Crie loaders por tipo de entidade/consulta (ex.:
postsByUserIdLoader). - Instancie os loaders por requisição, não como singleton global.
- Injete loaders no contexto do graphql (ex.:
context.loaders). - Nos resolvers, use
load()/loadMany()em vez de buscar diretamente no banco.
Esse detalhe “por requisição” é crítico para evitar vazamento de dados entre usuários e para manter o cache limitado ao request atual.
Cuidados importantes com cache e segurança
Em cyber segurança, DataLoader merece atenção por dois motivos:
- Risco de vazamento por cache global: se o loader for compartilhado entre requisições (singleton), o cache pode servir dados de um usuário para outro, especialmente quando as chaves não incorporam o escopo de autorização (ex.:
userIdem multi-tenant). - Autorização em nível de campo: graphql incentiva consultas complexas; se a autorização ficar “espalhada” e for aplicada depois do batch, você pode carregar dados demais e filtrar tarde, gerando exposição acidental em logs, traces ou erros.
Boas práticas:
- Crie loaders no
contextpor request. - Inclua o tenantId/escopo quando necessário na chave do cache (ou separe loaders por tenant).
- Valide autorização antes de retornar dados (e, quando possível, antes de buscar).
- Evite mensagens de erro detalhadas com dados sensíveis em produção.
DataLoader não é “cura para tudo”
DataLoader ajuda principalmente em cargas por chave (ex.: por IDs). Para casos como:
- filtros complexos,
- ordenações por agregações,
- paginação avançada, o ideal é projetar resolvers que façam consultas otimizadas (por exemplo, com joins, views, materializações, ou query builders bem definidos) e considerar limites de complexidade.
Quando GraphQL Federation entra em cena
Se o DataLoader resolve um problema de performance em tempo de execução, GraphQL Federation ataca um problema de arquitetura e escala organizacional: quando a API graphql cresce a ponto de ser mantida por múltiplos times e domínios.
Em vez de um “monolito graphql”, a federação permite que você tenha:
- Subgraphs (serviços graphql menores) responsáveis por um domínio (ex.:
users,catalog,billing) - Um gateway que compõe um schema único para o cliente
O cliente continua vendo um único endpoint graphql, mas internamente o gateway roteia partes da query para os subgraphs necessários e junta o resultado.
Benefícios típicos
- Autonomia por domínio: cada time evolui seu subgraph sem coordenar mudanças em um repositório único.
- Escala de desenvolvimento: evita gargalos de PR e releases centralizados.
- Separação de responsabilidade: domínios com ciclos diferentes (ex.: catálogo vs. pagamentos) podem ter deploys independentes.
Custos e complexidade que precisam ser considerados
Federation é útil, mas adiciona camadas:
- Mais hops de rede (gateway → subgraph), com impacto em latência.
- Observabilidade mais difícil (tracing distribuído passa a ser obrigatório).
- Governança de schema e compatibilidade (breaking changes ficam mais perigosas).
- Possibilidade de reintroduzir N+1 “entre serviços” se a composição exigir muitas chamadas pequenas.
DataLoader e Federation: como eles se combinam
É comum precisar dos dois:
- Dentro de um subgraph, DataLoader reduz N+1 no acesso a banco/serviços internos.
- No nível do gateway, você precisa cuidar para não criar padrões de consulta que disparem múltiplas resoluções remotas redundantes.
Um cenário frequente em federação:
- Uma entidade é “estendida” em outro subgraph (ex.:
Userdefinido emusers, mas estendido embillingcominvoices). - O gateway pode precisar consultar
userspara resolver referências e depoisbillingpara completar campos.
Se essas resoluções remotas ocorrerem item a item, o N+1 aparece como N+1 de rede. Dependendo da stack, é importante garantir que haja batching para resoluções de entidades (por exemplo, resolvers que aceitam listas de chaves) e que o gateway/subgraphs estejam configurados para agrupar operações quando possível.
Controles de segurança recomendados em APIs GraphQL (especialmente federadas)
GraphQL amplia a superfície de ataque por permitir consultas profundas e altamente customizáveis. Em federação, isso se soma à complexidade distribuída.
Controles práticos:
-
Limite de complexidade e profundidade
- Defina custo por campo e limite por operação.
- Bloqueie recursão e profundidades exageradas.
-
Persisted Queries (quando aplicável)
- Reduz exploração por ad hoc queries em produção.
- Ajuda a controlar o que pode ser executado.
-
Rate limiting e quotas por consumidor
- Por token, por IP, por app, por tenant.
- Diferencie limites para operações caras.
-
Autorização consistente
- Padronize autenticação/claims no gateway e repasse confiável aos subgraphs.
- Garanta autorização em nível de objeto/campo quando necessário.
-
Observabilidade e auditoria
- Logging estruturado por operação (nome da operação, complexidade, duração).
- Tracing distribuído (gateway e subgraphs).
- Alertas para spikes de complexidade/latência.
-
Hardening de introspecção
- Em produção, avalie restringir introspecção para clientes não confiáveis, ou condicionar por ambiente/role.
Como decidir: DataLoader, Federation ou ambos?
Um guia objetivo:
-
Você tem latência alta e muitas queries repetidas por request, principalmente em relacionamentos?
→ Priorize DataLoader e revisão de resolvers. -
Você tem múltiplos times, domínios bem definidos e um schema grande difícil de evoluir em um único serviço?
→ Considere GraphQL Federation (com gateway, governança e observabilidade). -
Você tem federação e sofre com excesso de chamadas entre serviços e lentidão em campos “estendidos”?
→ Combine Federation + batching (estratégias semelhantes ao DataLoader, mas também para resoluções remotas).
Conclusão
Em graphql, performance e arquitetura caminham juntas. O N+1 Problem é uma consequência comum do modelo de resolvers e precisa ser tratado cedo, com ferramentas como DataLoader (batching e cache por requisição) e com desenho cuidadoso de consultas e autorização.
Quando a API cresce em escopo e equipes, GraphQL Federation permite escalar o desenvolvimento com subgraphs por domínio — desde que você aceite a complexidade adicional em observabilidade, governança e segurança. Na prática, sistemas maduros costumam usar os dois: DataLoader para eficiência local e Federation para escala organizacional, com controles claros para evitar N+1 “de rede” e reduzir a superfície de ataque.