# 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. - Published: 2026-03-25 - Updated: 2026-05-04 - Author: SharpSkill - Tags: spring graphql, dataloaders, n+1 problem, interview, graphql java - Reading time: 14 min --- 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. > **Foco de la entrevista** > > 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. ```java // build.gradle.kts 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`. ```graphql # 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. ```java // ArticleController.java @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
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. > **Trampa clásica de entrevista** > > 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: ```graphql # 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 ```sql -- 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 adicionales ``` Esta 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. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Método de carga por lotes public Mono> loadAuthors(Set authorIds) { // Consulta única para todos los autores List authors = authorRepository.findAllById(authorIds); // Transformación a Map para búsqueda rápida Map 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. ```java // GraphQLConfig.java @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: ```java // ArticleController.java @Controller public class ArticleController { private final ArticleRepository articleRepository; public ArticleController(ArticleRepository articleRepository) { this.articleRepository = articleRepository; } @QueryMapping public List
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( Article article, DataLoader 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: ```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) ``` ## ¿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. ```java // SecuredAuthorBatchLoader.java @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> loadAuthorsForTenant( Set authorIds, BatchLoaderEnvironment env) { // Recuperar el contexto GraphQL GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Consulta filtrada por tenant List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map 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: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono 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 | ```java // EquivalenceDemo.java @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. ```graphql # 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: ```java // ArticleMutationController.java @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; } } ``` ```java // CreateArticleInput.java 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 ) {} ``` > **Gestión de errores** > > 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. ```java // OptimizedArticleController.java @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
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 author(List
articles) { // Recopilar los IDs únicos de autores Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Consulta única para todos los autores Map 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> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Recuperación por lotes de comentarios List allComments = commentRepository .findByArticleIdIn(articleIds); // Agrupación por artículo Map> 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. ```java // ArticleControllerTest.java @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(); } } ``` ## ¿Cómo gestionar las suscripciones en tiempo real? Las suscripciones permiten enviar datos en tiempo real a los clientes a través de WebSocket. ```graphql # schema.graphqls type Subscription { articleCreated: Article! commentAdded(articleId: ID!): Comment! } ``` ```java // SubscriptionController.java @Controller public class SubscriptionController { private final Sinks.Many
articleSink = Sinks.many().multicast().onBackpressureBuffer(); private final Sinks.Many commentSink = Sinks.many().multicast().onBackpressureBuffer(); @SubscriptionMapping public Flux
articleCreated() { return articleSink.asFlux(); } @SubscriptionMapping public Flux 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: ```java // build.gradle.kts dependencies { implementation("org.springframework.boot:spring-boot-starter-websocket") } ``` ```java // WebSocketConfig.java @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. ```java // SecurityConfig.java @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(); } } ``` ```java // SecuredArticleController.java @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
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: ```yaml # application.yml spring: graphql: schema: introspection: # Desactivar la introspección en producción enabled: false ``` ```java // QueryComplexityInstrumentation.java @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 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`, `@MutationMapping` y `@SchemaMapping` - ✅ Describir el problema N+1 y su impacto en el rendimiento GraphQL - ✅ Implementar un DataLoader con `@BatchMapping` o BatchLoaderRegistry - ✅ Validar las entradas de mutación con Bean Validation - ✅ Asegurar los resolvers con `@PreAuthorize` y Spring Security - ✅ Probar los resolvers con GraphQlTester - ✅ Configurar las suscripciones para funcionalidades en tiempo real - ✅ Proteger la API contra consultas demasiado complejas --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1