Entrevista Spring GraphQL: Resolvers, DataLoaders e Soluções para o Problema N+1
Prepare-se para entrevistas Spring GraphQL com este guia completo. Resolvers, DataLoaders, gestão do problema N+1, mutations e melhores práticas para perguntas técnicas.

Spring for GraphQL simplifica a integração do GraphQL em aplicações Spring Boot. Esta tecnologia tornou-se indispensável para APIs modernas, e as perguntas de entrevista sobre o assunto são cada vez mais frequentes. Este guia aborda os conceitos fundamentais: resolvers, DataLoaders, problema N+1 e padrões avançados.
Os recrutadores avaliam particularmente a compreensão do problema N+1 e o uso de DataLoaders. Esses dois temas representam 60% das perguntas em entrevistas Spring GraphQL.
O que é Spring for GraphQL?
Spring for GraphQL é o sucessor oficial do GraphQL Java Spring, integrado nativamente no framework Spring desde a versão 2.7. Esta integração traz várias vantagens: suporte às anotações Spring, integração com Spring Security e uso transparente de WebFlux ou MVC.
A configuração básica requer apenas a dependência spring-boot-starter-graphql e um schema GraphQL.
dependencies {
// Spring GraphQL starter com Spring MVC
implementation("org.springframework.boot:spring-boot-starter-graphql")
implementation("org.springframework.boot:spring-boot-starter-web")
// Para os testes GraphQL
testImplementation("org.springframework.graphql:spring-graphql-test")
}O schema GraphQL fica em src/main/resources/graphql/ com a extensão .graphqls.
# schema.graphqls
type Query {
# Recuperar um artigo pelo seu identificador
article(id: ID!): Article
# Lista paginada de artigos
articles(page: Int = 0, size: Int = 10): [Article!]!
}
type Article {
id: ID!
title: String!
content: String!
# Relação com o autor
author: Author!
# Lista de comentários
comments: [Comment!]!
createdAt: String!
}
type Author {
id: ID!
name: String!
email: String!
# Artigos escritos por este autor
articles: [Article!]!
}
type Comment {
id: ID!
content: String!
author: Author!
createdAt: String!
}Esta arquitetura declara os tipos e suas relações. O GraphQL gera automaticamente a documentação e valida as queries do lado do servidor.
Como funcionam os Resolvers Spring GraphQL?
Os resolvers constituem o coração da execução GraphQL. Cada campo do schema pode ter um resolver dedicado. Spring usa a anotação @QueryMapping para as queries raiz e @SchemaMapping para as relações.
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
public ArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
}
// Resolver para Query.article(id)
@QueryMapping
public Article article(@Argument Long id) {
return articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}
// Resolver para Query.articles(page, size)
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Resolver para Article.author - chamado para cada artigo
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return authorRepository.findById(article.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
}
}O resolver author é executado para cada artigo retornado. Esta arquitetura flexível carrega os dados sob demanda mas introduz o problema N+1.
Um candidato que implementa um resolver @SchemaMapping sem mencionar o problema N+1 demonstra uma compreensão incompleta do GraphQL. Os recrutadores esperam sistematicamente esta análise.
O que é o Problema N+1 no GraphQL?
O problema N+1 surge quando uma query GraphQL desencadeia N queries adicionais para carregar as relações. Este padrão destrutivo ocorre sistematicamente com resolvers básicos.
Considere uma query que recupera 50 artigos com seus autores:
# Query GraphQL
query {
articles(size: 50) {
id
title
author {
name
}
}
}Com o resolver anterior, esta query executa:
- 1 query para os 50 artigos
- 50 queries para carregar cada autor individualmente
-- Query 1: recuperação de artigos
SELECT * FROM articles LIMIT 50
-- Queries 2-51: cada autor individualmente
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... 47 queries adicionaisEsta multiplicação de queries degrada drasticamente o desempenho. Um endpoint que responde em 50ms pode saltar para 2 segundos com N+1.
Como funcionam os DataLoaders?
Os DataLoaders agrupam queries individuais em requisições em lote. Em vez de carregar cada autor separadamente, o DataLoader coleta todos os IDs solicitados e executa uma única query.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Método de carregamento em lote
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Query única para todos os autores
List<Author> authors = authorRepository.findAllById(authorIds);
// Transformação em Map para busca rápida
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}O registro do DataLoader é feito via BatchLoaderRegistry em uma configuração dedicada.
@Configuration
public class GraphQLConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(
AuthorBatchLoader authorBatchLoader) {
return registry -> {
// Registro do DataLoader para os autores
registry.forTypePair(Long.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
authorBatchLoader.loadAuthors(authorIds));
};
}
}O resolver modificado agora utiliza o DataLoader em vez de acesso direto:
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
public ArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Resolver otimizado com DataLoader
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> authorDataLoader) {
// O DataLoader agrupa automaticamente as chamadas
return authorDataLoader.load(article.getAuthorId());
}
}Com esta abordagem, a mesma query para 50 artigos executa apenas 2 queries SQL:
-- Query 1: recuperação de artigos
SELECT * FROM articles LIMIT 50
-- Query 2: todos os autores de uma vez
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)Pronto para mandar bem nas entrevistas de Spring Boot?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Como implementar um DataLoader com contexto?
DataLoaders avançados às vezes requerem contexto adicional, como um identificador de tenant para aplicações multi-tenant ou filtros de segurança.
@Component
public class SecuredAuthorBatchLoader {
private final AuthorRepository authorRepository;
private final SecurityContextHolder securityContext;
public SecuredAuthorBatchLoader(AuthorRepository authorRepository,
SecurityContextHolder securityContext) {
this.authorRepository = authorRepository;
this.securityContext = securityContext;
}
public Mono<Map<Long, Author>> loadAuthorsForTenant(
Set<Long> authorIds,
BatchLoaderEnvironment env) {
// Recuperar o contexto GraphQL
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Query filtrada por tenant
List<Author> authors = authorRepository
.findAllByIdInAndTenantId(authorIds, tenantId);
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}A configuração do contexto é feita por meio de um interceptor WebGraphQL:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request,
Chain chain) {
// Extrair o tenant dos headers
String tenantId = request.getHeaders()
.getFirst("X-Tenant-ID");
// Adicionar ao contexto GraphQL
request.configureExecutionInput((input, builder) ->
builder.graphQLContext(ctx ->
ctx.put("tenantId", tenantId)
).build()
);
return chain.next(request);
}
}Quais são as diferenças entre @QueryMapping e @SchemaMapping?
Esta pergunta clássica de entrevista verifica a compreensão da hierarquia de resolvers.
| Anotação | Uso | Equivalente |
|----------|-----|-------------|
| @QueryMapping | Campos raiz do tipo Query | @SchemaMapping(typeName = "Query") |
| @MutationMapping | Campos raiz do tipo Mutation | @SchemaMapping(typeName = "Mutation") |
| @SubscriptionMapping | Subscriptions em tempo real | @SchemaMapping(typeName = "Subscription") |
| @SchemaMapping | Todos os campos de qualquer tipo | Forma genérica |
@Controller
public class EquivalenceDemo {
// Estas duas declarações são equivalentes
@QueryMapping
public Article article(@Argument Long id) {
return findArticle(id);
}
@SchemaMapping(typeName = "Query", field = "article")
public Article articleEquivalent(@Argument Long id) {
return findArticle(id);
}
// Para campos aninhados, apenas @SchemaMapping funciona
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return findAuthor(article.getAuthorId());
}
// Sintaxe alternativa com o tipo como parâmetro
@SchemaMapping
public Author author(Article article) {
// Spring infere typeName = "Article" do parâmetro
return findAuthor(article.getAuthorId());
}
}Como gerenciar mutations com validação?
As mutations GraphQL modificam os dados. Spring GraphQL integra-se com Bean Validation para validar as entradas.
# schema.graphqls
type Mutation {
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Article!
deleteArticle(id: ID!): Boolean!
}
input CreateArticleInput {
title: String!
content: String!
authorId: ID!
}
input UpdateArticleInput {
title: String
content: String
}O controller usa @Valid para acionar a validação:
@Controller
public class ArticleMutationController {
private final ArticleService articleService;
public ArticleMutationController(ArticleService articleService) {
this.articleService = articleService;
}
@MutationMapping
public Article createArticle(@Argument @Valid CreateArticleInput input) {
// A validação é executada automaticamente
return articleService.create(input);
}
@MutationMapping
public Article updateArticle(@Argument Long id,
@Argument @Valid UpdateArticleInput input) {
return articleService.update(id, input);
}
@MutationMapping
public boolean deleteArticle(@Argument Long id) {
articleService.delete(id);
return true;
}
}public record CreateArticleInput(
@NotBlank(message = "O título é obrigatório")
@Size(min = 5, max = 200, message = "O título deve ter entre 5 e 200 caracteres")
String title,
@NotBlank(message = "O conteúdo é obrigatório")
@Size(min = 100, message = "O conteúdo deve ter pelo menos 100 caracteres")
String content,
@NotNull(message = "O autor é obrigatório")
Long authorId
) {}Os erros de validação retornam automaticamente um erro GraphQL estruturado com o caminho do campo inválido. Spring GraphQL formata esses erros de acordo com a especificação GraphQL.
Como otimizar queries com @BatchMapping?
A anotação @BatchMapping simplifica a criação de DataLoaders diretamente nos controllers. Esta abordagem evita a configuração explícita do BatchLoaderRegistry.
@Controller
public class OptimizedArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
private final CommentRepository commentRepository;
public OptimizedArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository,
CommentRepository commentRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
this.commentRepository = commentRepository;
}
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// BatchMapping para os autores - processa todos os artigos de uma vez
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Coletar os IDs únicos de autores
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Query única para todos os autores
Map<Long, Author> authorsById = authorRepository
.findAllById(authorIds)
.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
// Associação artigo -> autor
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> authorsById.get(article.getAuthorId())
));
}
// BatchMapping para os comentários
@BatchMapping
public Map<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Recuperação em lote dos comentários
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Agrupamento por artigo
Map<Long, List<Comment>> commentsByArticle = allComments.stream()
.collect(Collectors.groupingBy(Comment::getArticleId));
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> commentsByArticle
.getOrDefault(article.getId(), List.of())
));
}
}@BatchMapping oferece uma sintaxe mais concisa do que a configuração manual de DataLoaders, mantendo as mesmas garantias de desempenho.
Como testar Resolvers GraphQL?
Spring GraphQL fornece GraphQlTester para escrever testes expressivos e legíveis.
@SpringBootTest
@AutoConfigureGraphQlTester
class ArticleControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldReturnArticleById() {
// Given: documento GraphQL
String query = """
query {
article(id: 1) {
id
title
author {
name
}
}
}
""";
// When & Then
graphQlTester.document(query)
.execute()
.path("article.id").entity(String.class).isEqualTo("1")
.path("article.title").entity(String.class).isNotEmpty()
.path("article.author.name").entity(String.class).isNotEmpty();
}
@Test
void shouldCreateArticle() {
String mutation = """
mutation {
createArticle(input: {
title: "Novo título",
content: "Conteúdo de teste suficientemente longo para validar as restrições",
authorId: 1
}) {
id
title
}
}
""";
graphQlTester.document(mutation)
.execute()
.path("createArticle.id").entity(String.class).isNotEmpty()
.path("createArticle.title").entity(String.class)
.isEqualTo("Novo título");
}
@Test
void shouldReturnErrorForInvalidInput() {
String mutation = """
mutation {
createArticle(input: {
title: "",
content: "curto",
authorId: 1
}) {
id
}
}
""";
graphQlTester.document(mutation)
.execute()
.errors()
.expect(error -> error.getMessage()
.contains("O título é obrigatório"));
}
@Test
void shouldHandleVariables() {
String query = """
query GetArticle($id: ID!) {
article(id: $id) {
id
title
}
}
""";
graphQlTester.document(query)
.variable("id", 42)
.execute()
.path("article").valueIsNull();
}
}Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Como gerenciar subscriptions em tempo real?
As subscriptions permitem enviar dados em tempo real aos clientes via WebSocket.
# schema.graphqls
type Subscription {
articleCreated: Article!
commentAdded(articleId: ID!): Comment!
}@Controller
public class SubscriptionController {
private final Sinks.Many<Article> articleSink =
Sinks.many().multicast().onBackpressureBuffer();
private final Sinks.Many<Comment> commentSink =
Sinks.many().multicast().onBackpressureBuffer();
@SubscriptionMapping
public Flux<Article> articleCreated() {
return articleSink.asFlux();
}
@SubscriptionMapping
public Flux<Comment> commentAdded(@Argument Long articleId) {
return commentSink.asFlux()
.filter(comment -> comment.getArticleId().equals(articleId));
}
// Método chamado ao criar um artigo
public void publishArticle(Article article) {
articleSink.tryEmitNext(article);
}
// Método chamado ao adicionar um comentário
public void publishComment(Comment comment) {
commentSink.tryEmitNext(comment);
}
}A configuração WebSocket requer uma dependência adicional:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}Quais são as melhores práticas de segurança GraphQL?
A segurança GraphQL requer várias camadas de proteção contra ataques de negação de serviço e acessos não autorizados.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// Endpoint GraphQL protegido
.requestMatchers("/graphql").authenticated()
// GraphiQL acessível apenas em dev
.requestMatchers("/graphiql").permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}@Controller
public class SecuredArticleController {
private final ArticleRepository articleRepository;
public SecuredArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
// Acesso restrito a usuários autenticados
@QueryMapping
@PreAuthorize("isAuthenticated()")
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// Apenas administradores podem excluir
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public boolean deleteArticle(@Argument Long id) {
articleRepository.deleteById(id);
return true;
}
// Verificação granular: apenas o autor pode modificar
@MutationMapping
@PreAuthorize("@articleSecurity.isAuthor(#id, authentication)")
public Article updateArticle(@Argument Long id,
@Argument UpdateArticleInput input) {
return articleRepository.findById(id)
.map(article -> {
article.setTitle(input.title());
article.setContent(input.content());
return articleRepository.save(article);
})
.orElseThrow();
}
}Proteção contra queries complexas:
# application.yml
spring:
graphql:
schema:
introspection:
# Desativar a introspecção em produção
enabled: false@Component
public class QueryComplexityInstrumentation extends SimplePerformantInstrumentation {
private static final int MAX_COMPLEXITY = 100;
@Override
public InstrumentationState createState(InstrumentationCreateStateParameters parameters) {
return new ComplexityState();
}
@Override
public InstrumentationContext<ExecutionResult> beginExecution(
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
int complexity = calculateComplexity(parameters.getDocument());
if (complexity > MAX_COMPLEXITY) {
throw new GraphQLException(
"Query muito complexa: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
);
}
return super.beginExecution(parameters, state);
}
}Conclusão
Spring for GraphQL oferece uma integração elegante do GraphQL no ecossistema Spring. O domínio dos resolvers, dos DataLoaders e da gestão do problema N+1 constitui a base de conhecimentos esperada em entrevistas.
Checklist de entrevista Spring GraphQL:
- ✅ Explicar o papel das anotações
@QueryMapping,@MutationMappinge@SchemaMapping - ✅ Descrever o problema N+1 e seu impacto no desempenho GraphQL
- ✅ Implementar um DataLoader com
@BatchMappingou BatchLoaderRegistry - ✅ Validar as entradas de mutation com Bean Validation
- ✅ Proteger os resolvers com
@PreAuthorizee Spring Security - ✅ Testar os resolvers com GraphQlTester
- ✅ Configurar subscriptions para funcionalidades em tempo real
- ✅ Proteger a API contra queries muito complexas
Tags
Compartilhar
Artigos relacionados

Entrevista Spring Boot: Propagação de Transações
Domine a propagação de transações no Spring Boot: REQUIRED, REQUIRES_NEW, NESTED e mais. 12 perguntas de entrevista com código e armadilhas comuns.

Soluções N+1 no Spring Data JPA em 2026: Fetch Join e EntityGraph
Guia completo para detectar e corrigir o problema N+1 no Spring Data JPA. Fetch join, @EntityGraph, batch fetching e estratégias de performance de consultas.

30 Perguntas de Entrevista sobre Spring Boot: Guia Completo para Desenvolvedores Java
Prepare-se para suas entrevistas de Spring Boot com estas 30 perguntas essenciais sobre auto-configuração, starters, Spring Data JPA, segurança e testes.