Entrevista Spring GraphQL: Resolvers, DataLoaders y Soluciones al Problema N+1
Prepárate para entrevistas Spring GraphQL con esta guía completa. Resolvers, DataLoaders, gestión del problema N+1, mutaciones y mejores prácticas para preguntas técnicas.

Spring for GraphQL simplifica la integración de GraphQL en aplicaciones Spring Boot. Esta tecnología se ha vuelto indispensable para las APIs modernas, y las preguntas de entrevista sobre este tema son cada vez más frecuentes. Esta guía cubre los conceptos fundamentales: resolvers, DataLoaders, problema N+1 y patrones avanzados.
Los reclutadores evalúan especialmente la comprensión del problema N+1 y el uso de DataLoaders. Estos dos temas representan el 60% de las preguntas en entrevistas Spring GraphQL.
¿Qué es Spring for GraphQL?
Spring for GraphQL es el sucesor oficial de GraphQL Java Spring, integrado nativamente en el framework Spring desde la versión 2.7. Esta integración aporta varias ventajas: soporte de las anotaciones Spring, integración con Spring Security y uso transparente de WebFlux o MVC.
La configuración básica solo requiere la dependencia spring-boot-starter-graphql y un schema GraphQL.
dependencies {
// Spring GraphQL starter con Spring MVC
implementation("org.springframework.boot:spring-boot-starter-graphql")
implementation("org.springframework.boot:spring-boot-starter-web")
// Para los tests GraphQL
testImplementation("org.springframework.graphql:spring-graphql-test")
}El schema GraphQL se ubica en src/main/resources/graphql/ con la extensión .graphqls.
# schema.graphqls
type Query {
# Recuperar un artículo por su identificador
article(id: ID!): Article
# Lista paginada de artículos
articles(page: Int = 0, size: Int = 10): [Article!]!
}
type Article {
id: ID!
title: String!
content: String!
# Relación con el autor
author: Author!
# Lista de comentarios
comments: [Comment!]!
createdAt: String!
}
type Author {
id: ID!
name: String!
email: String!
# Artículos escritos por este autor
articles: [Article!]!
}
type Comment {
id: ID!
content: String!
author: Author!
createdAt: String!
}Esta arquitectura declara los tipos y sus relaciones. GraphQL genera automáticamente la documentación y valida las consultas del lado del servidor.
¿Cómo funcionan los Resolvers en Spring GraphQL?
Los resolvers constituyen el corazón de la ejecución GraphQL. Cada campo del schema puede tener un resolver dedicado. Spring utiliza la anotación @QueryMapping para las consultas raíz y @SchemaMapping para las relaciones.
@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 - llamado para cada artículo
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return authorRepository.findById(article.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
}
}El resolver author se ejecuta para cada artículo devuelto. Esta arquitectura flexible carga los datos bajo demanda pero introduce el problema N+1.
Un candidato que implementa un resolver @SchemaMapping sin mencionar el problema N+1 demuestra una comprensión incompleta de GraphQL. Los reclutadores esperan sistemáticamente este análisis.
¿Qué es el Problema N+1 en GraphQL?
El problema N+1 surge cuando una consulta GraphQL desencadena N consultas adicionales para cargar las relaciones. Este patrón destructivo aparece sistemáticamente con resolvers básicos.
Considera una consulta que recupera 50 artículos con sus autores:
# Consulta GraphQL
query {
articles(size: 50) {
id
title
author {
name
}
}
}Con el resolver anterior, esta consulta ejecuta:
- 1 consulta para los 50 artículos
- 50 consultas para cargar cada autor individualmente
-- Consulta 1: recuperación de artículos
SELECT * FROM articles LIMIT 50
-- Consultas 2-51: cada autor individualmente
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... 47 consultas adicionalesEsta multiplicación de consultas degrada drásticamente el rendimiento. Un endpoint que responde en 50ms puede saltar a 2 segundos con N+1.
¿Cómo funcionan los DataLoaders?
Los DataLoaders agrupan las consultas individuales en peticiones por lotes. En lugar de cargar cada autor por separado, el DataLoader recopila todos los IDs solicitados y ejecuta una única consulta.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Método de carga por lotes
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Consulta única para todos los autores
List<Author> authors = authorRepository.findAllById(authorIds);
// Transformación a Map para búsqueda rápida
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}El registro del DataLoader se realiza mediante BatchLoaderRegistry en una configuración dedicada.
@Configuration
public class GraphQLConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(
AuthorBatchLoader authorBatchLoader) {
return registry -> {
// Registro del DataLoader para los autores
registry.forTypePair(Long.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
authorBatchLoader.loadAuthors(authorIds));
};
}
}El resolver modificado utiliza ahora el DataLoader en lugar del acceso directo:
@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 optimizado con DataLoader
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> authorDataLoader) {
// El DataLoader agrupa automáticamente las llamadas
return authorDataLoader.load(article.getAuthorId());
}
}Con este enfoque, la misma consulta para 50 artículos ejecuta solo 2 consultas SQL:
-- Consulta 1: recuperación de artículos
SELECT * FROM articles LIMIT 50
-- Consulta 2: todos los autores de una vez
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)¿Listo para aprobar tus entrevistas de Spring Boot?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
¿Cómo implementar un DataLoader con contexto?
Los DataLoaders avanzados a veces requieren contexto adicional, como un identificador de tenant para aplicaciones multi-tenant o filtros de seguridad.
@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 el contexto GraphQL
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Consulta 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);
}
}La configuración del contexto se realiza mediante un interceptor WebGraphQL:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request,
Chain chain) {
// Extraer el tenant de los headers
String tenantId = request.getHeaders()
.getFirst("X-Tenant-ID");
// Añadir al contexto GraphQL
request.configureExecutionInput((input, builder) ->
builder.graphQLContext(ctx ->
ctx.put("tenantId", tenantId)
).build()
);
return chain.next(request);
}
}¿Cuáles son las diferencias entre @QueryMapping y @SchemaMapping?
Esta pregunta clásica de entrevista verifica la comprensión de la jerarquía de resolvers.
| Anotación | Uso | Equivalente |
|-----------|-----|-------------|
| @QueryMapping | Campos raíz del tipo Query | @SchemaMapping(typeName = "Query") |
| @MutationMapping | Campos raíz del tipo Mutation | @SchemaMapping(typeName = "Mutation") |
| @SubscriptionMapping | Suscripciones en tiempo real | @SchemaMapping(typeName = "Subscription") |
| @SchemaMapping | Todos los campos de cualquier tipo | Forma genérica |
@Controller
public class EquivalenceDemo {
// Estas dos declaraciones son 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 los campos anidados, solo @SchemaMapping funciona
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return findAuthor(article.getAuthorId());
}
// Sintaxis alternativa con el tipo como parámetro
@SchemaMapping
public Author author(Article article) {
// Spring infiere typeName = "Article" del parámetro
return findAuthor(article.getAuthorId());
}
}¿Cómo gestionar las mutaciones con validación?
Las mutaciones GraphQL modifican los datos. Spring GraphQL se integra con Bean Validation para validar las 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
}El controller utiliza @Valid para activar la validación:
@Controller
public class ArticleMutationController {
private final ArticleService articleService;
public ArticleMutationController(ArticleService articleService) {
this.articleService = articleService;
}
@MutationMapping
public Article createArticle(@Argument @Valid CreateArticleInput input) {
// La validación se ejecuta automáticamente
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 = "El título es obligatorio")
@Size(min = 5, max = 200, message = "El título debe tener entre 5 y 200 caracteres")
String title,
@NotBlank(message = "El contenido es obligatorio")
@Size(min = 100, message = "El contenido debe tener al menos 100 caracteres")
String content,
@NotNull(message = "El autor es obligatorio")
Long authorId
) {}Los errores de validación devuelven automáticamente un error GraphQL estructurado con la ruta del campo no válido. Spring GraphQL formatea estos errores según la especificación GraphQL.
¿Cómo optimizar las consultas con @BatchMapping?
La anotación @BatchMapping simplifica la creación de DataLoaders directamente en los controllers. Este enfoque evita la configuración explícita del 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 los autores - procesa todos los artículos a la vez
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Recopilar los IDs únicos de autores
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Consulta única para todos los autores
Map<Long, Author> authorsById = authorRepository
.findAllById(authorIds)
.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
// Asociación artículo -> autor
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> authorsById.get(article.getAuthorId())
));
}
// BatchMapping para los comentarios
@BatchMapping
public Map<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Recuperación por lotes de comentarios
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Agrupación por artículo
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 ofrece una sintaxis más concisa que la configuración manual de DataLoaders, manteniendo las mismas garantías de rendimiento.
¿Cómo probar los Resolvers GraphQL?
Spring GraphQL proporciona GraphQlTester para escribir tests expresivos y legibles.
@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: "Nuevo título",
content: "Contenido de prueba lo suficientemente largo para validar las restricciones",
authorId: 1
}) {
id
title
}
}
""";
graphQlTester.document(mutation)
.execute()
.path("createArticle.id").entity(String.class).isNotEmpty()
.path("createArticle.title").entity(String.class)
.isEqualTo("Nuevo título");
}
@Test
void shouldReturnErrorForInvalidInput() {
String mutation = """
mutation {
createArticle(input: {
title: "",
content: "corto",
authorId: 1
}) {
id
}
}
""";
graphQlTester.document(mutation)
.execute()
.errors()
.expect(error -> error.getMessage()
.contains("El título es obligatorio"));
}
@Test
void shouldHandleVariables() {
String query = """
query GetArticle($id: ID!) {
article(id: $id) {
id
title
}
}
""";
graphQlTester.document(query)
.variable("id", 42)
.execute()
.path("article").valueIsNull();
}
}¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
¿Cómo gestionar las suscripciones en tiempo real?
Las suscripciones permiten enviar datos en tiempo real a los clientes a través de 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 llamado al crear un artículo
public void publishArticle(Article article) {
articleSink.tryEmitNext(article);
}
// Método llamado al añadir un comentario
public void publishComment(Comment comment) {
commentSink.tryEmitNext(comment);
}
}La configuración WebSocket requiere una dependencia adicional:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}¿Cuáles son las mejores prácticas de seguridad GraphQL?
La seguridad GraphQL requiere varias capas de protección contra los ataques de denegación de servicio y los accesos no 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 accesible solo en 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;
}
// Acceso restringido a usuarios autenticados
@QueryMapping
@PreAuthorize("isAuthenticated()")
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// Solo los administradores pueden eliminar
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public boolean deleteArticle(@Argument Long id) {
articleRepository.deleteById(id);
return true;
}
// Verificación granular: solo el autor puede 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();
}
}Protección contra consultas complejas:
# application.yml
spring:
graphql:
schema:
introspection:
# Desactivar la introspección en producción
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(
"Consulta demasiado compleja: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
);
}
return super.beginExecution(parameters, state);
}
}Conclusión
Spring for GraphQL ofrece una integración elegante de GraphQL en el ecosistema Spring. El dominio de los resolvers, los DataLoaders y la gestión del problema N+1 constituye la base de conocimientos esperada en entrevistas.
Checklist de entrevista Spring GraphQL:
- ✅ Explicar el rol de las anotaciones
@QueryMapping,@MutationMappingy@SchemaMapping - ✅ Describir el problema N+1 y su impacto en el rendimiento GraphQL
- ✅ Implementar un DataLoader con
@BatchMappingo BatchLoaderRegistry - ✅ Validar las entradas de mutación con Bean Validation
- ✅ Asegurar los resolvers con
@PreAuthorizey Spring Security - ✅ Probar los resolvers con GraphQlTester
- ✅ Configurar las suscripciones para funcionalidades en tiempo real
- ✅ Proteger la API contra consultas demasiado complejas
Etiquetas
Compartir
Artículos relacionados

Entrevista Spring Boot: Propagación de Transacciones
Domina la propagación de transacciones en Spring Boot: REQUIRED, REQUIRES_NEW, NESTED y más. 12 preguntas de entrevista con código y trampas comunes.

Soluciones N+1 en Spring Data JPA en 2026: Fetch Join y EntityGraph
Guía completa para detectar y corregir el problema N+1 en Spring Data JPA. Fetch join, @EntityGraph, batch fetching y estrategias de rendimiento de consultas.

30 Preguntas de Entrevista sobre Spring Boot: Guía Completa para Desarrolladores Java
Prepárate para tus entrevistas de Spring Boot con estas 30 preguntas esenciales sobre auto-configuración, starters, Spring Data JPA, seguridad y testing.