# Spring GraphQL Interview: Resolvers, DataLoaders and N+1 Problem Solutions > Prepare for Spring GraphQL interviews with this complete guide. Resolvers, DataLoaders, N+1 problem handling, mutations, and best practices for technical questions. - Published: 2026-03-25 - Updated: 2026-03-31 - Author: SharpSkill - Tags: spring graphql, dataloaders, n+1 problem, interview, graphql java - Reading time: 14 min --- Spring for GraphQL simplifies GraphQL integration in Spring Boot applications. This technology has become essential for modern APIs, and interview questions on this topic are increasingly common. This guide covers the core concepts: resolvers, DataLoaders, the N+1 problem, and advanced patterns. > **Interview Focus** > > Recruiters particularly test understanding of the N+1 problem and DataLoader usage. These two topics represent 60% of Spring GraphQL interview questions. ## What is Spring for GraphQL? Spring for GraphQL is the official successor to GraphQL Java Spring, natively integrated into the Spring framework since version 2.7. This integration brings several advantages: Spring annotation support, Spring Security integration, and transparent use of WebFlux or MVC. Basic configuration requires only the `spring-boot-starter-graphql` dependency and a GraphQL schema. ```java // build.gradle.kts dependencies { // Spring GraphQL starter with Spring MVC implementation("org.springframework.boot:spring-boot-starter-graphql") implementation("org.springframework.boot:spring-boot-starter-web") // For GraphQL tests testImplementation("org.springframework.graphql:spring-graphql-test") } ``` The GraphQL schema goes in `src/main/resources/graphql/` with the `.graphqls` extension. ```graphql # schema.graphqls type Query { # Retrieve an article by its identifier article(id: ID!): Article # Paginated list of articles articles(page: Int = 0, size: Int = 10): [Article!]! } type Article { id: ID! title: String! content: String! # Relationship to author author: Author! # List of comments comments: [Comment!]! createdAt: String! } type Author { id: ID! name: String! email: String! # Articles written by this author articles: [Article!]! } type Comment { id: ID! content: String! author: Author! createdAt: String! } ``` This architecture declares types and their relationships. GraphQL automatically generates documentation and validates queries server-side. ## How Do Spring GraphQL Resolvers Work? Resolvers form the core of GraphQL execution. Each schema field can have a dedicated resolver. Spring uses the `@QueryMapping` annotation for root queries and `@SchemaMapping` for relationships. ```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 for Query.article(id) @QueryMapping public Article article(@Argument Long id) { return articleRepository.findById(id) .orElseThrow(() -> new ArticleNotFoundException(id)); } // Resolver for 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 for Article.author - called for each article @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return authorRepository.findById(article.getAuthorId()) .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId())); } } ``` The `author` resolver executes for each returned article. This flexible architecture loads data on demand but introduces the N+1 problem. > **Classic Interview Trap** > > A candidate who implements a `@SchemaMapping` resolver without mentioning the N+1 problem demonstrates incomplete GraphQL understanding. Recruiters systematically expect this analysis. ## What is the N+1 Problem in GraphQL? The N+1 problem occurs when a GraphQL query triggers N additional queries to load relationships. This destructive pattern happens systematically with basic resolvers. Consider a query fetching 50 articles with their authors: ```graphql # GraphQL query query { articles(size: 50) { id title author { name } } } ``` With the previous resolver, this query executes: - 1 query for the 50 articles - 50 queries to load each author individually ```sql -- Query 1: fetch articles SELECT * FROM articles LIMIT 50 -- Queries 2-51: each author individually SELECT * FROM authors WHERE id = 1 SELECT * FROM authors WHERE id = 2 SELECT * FROM authors WHERE id = 3 -- ... 47 more queries ``` This query multiplication drastically degrades performance. An endpoint responding in 50ms can jump to 2 seconds with N+1. ## How Do DataLoaders Work? DataLoaders batch individual queries into bulk requests. Instead of loading each author separately, the DataLoader collects all requested IDs and executes a single query. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Batch loading method public Mono> loadAuthors(Set authorIds) { // Single query for all authors List authors = authorRepository.findAllById(authorIds); // Transform to Map for fast lookup Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` DataLoader registration happens via `BatchLoaderRegistry` in a dedicated configuration. ```java // GraphQLConfig.java @Configuration public class GraphQLConfig { @Bean public BatchLoaderRegistry batchLoaderRegistry( AuthorBatchLoader authorBatchLoader) { return registry -> { // Register DataLoader for authors registry.forTypePair(Long.class, Author.class) .registerMappedBatchLoader((authorIds, env) -> authorBatchLoader.loadAuthors(authorIds)); }; } } ``` The modified resolver now uses the DataLoader instead of direct access: ```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(); } // Optimized resolver with DataLoader @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader authorDataLoader) { // DataLoader automatically batches calls return authorDataLoader.load(article.getAuthorId()); } } ``` With this approach, the same query for 50 articles executes only 2 SQL queries: ```sql -- Query 1: fetch articles SELECT * FROM articles LIMIT 50 -- Query 2: all authors at once SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50) ``` ## How to Implement a DataLoader with Context? Advanced DataLoaders sometimes require additional context, such as tenant ID for multi-tenant applications or security filters. ```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) { // Retrieve GraphQL context GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Query filtered by tenant List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` Context configuration happens via a WebGraphQL interceptor: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono intercept( WebGraphQlRequest request, Chain chain) { // Extract tenant from headers String tenantId = request.getHeaders() .getFirst("X-Tenant-ID"); // Add to GraphQL context request.configureExecutionInput((input, builder) -> builder.graphQLContext(ctx -> ctx.put("tenantId", tenantId) ).build() ); return chain.next(request); } } ``` ## What Are the Differences Between @QueryMapping and @SchemaMapping? This classic interview question verifies understanding of the resolver hierarchy. | Annotation | Usage | Equivalent | |------------|-------|------------| | `@QueryMapping` | Root fields of Query type | `@SchemaMapping(typeName = "Query")` | | `@MutationMapping` | Root fields of Mutation type | `@SchemaMapping(typeName = "Mutation")` | | `@SubscriptionMapping` | Real-time subscriptions | `@SchemaMapping(typeName = "Subscription")` | | `@SchemaMapping` | All fields of any type | Generic form | ```java // EquivalenceDemo.java @Controller public class EquivalenceDemo { // These two declarations are equivalent @QueryMapping public Article article(@Argument Long id) { return findArticle(id); } @SchemaMapping(typeName = "Query", field = "article") public Article articleEquivalent(@Argument Long id) { return findArticle(id); } // For nested fields, only @SchemaMapping works @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return findAuthor(article.getAuthorId()); } // Alternative syntax with type as parameter @SchemaMapping public Author author(Article article) { // Spring infers typeName = "Article" from parameter return findAuthor(article.getAuthorId()); } } ``` ## How to Handle Mutations with Validation? GraphQL mutations modify data. Spring GraphQL integrates with Bean Validation to validate inputs. ```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 } ``` The controller uses `@Valid` to trigger validation: ```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) { // Validation executes automatically 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 = "Title is required") @Size(min = 5, max = 200, message = "Title must be between 5 and 200 characters") String title, @NotBlank(message = "Content is required") @Size(min = 100, message = "Content must be at least 100 characters") String content, @NotNull(message = "Author is required") Long authorId ) {} ``` > **Error Handling** > > Validation errors automatically return a structured GraphQL error with the invalid field path. Spring GraphQL formats these errors according to the GraphQL specification. ## How to Optimize Queries with @BatchMapping? The `@BatchMapping` annotation simplifies DataLoader creation directly in controllers. This approach avoids explicit BatchLoaderRegistry configuration. ```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 for authors - processes all articles at once @BatchMapping public Map author(List
articles) { // Collect unique author IDs Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Single query for all authors Map authorsById = authorRepository .findAllById(authorIds) .stream() .collect(Collectors.toMap(Author::getId, Function.identity())); // Article -> author association return articles.stream() .collect(Collectors.toMap( Function.identity(), article -> authorsById.get(article.getAuthorId()) )); } // BatchMapping for comments @BatchMapping public Map> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Batch fetch comments List allComments = commentRepository .findByArticleIdIn(articleIds); // Group by article 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` provides more concise syntax than manual DataLoader configuration while delivering the same performance guarantees. ## How to Test GraphQL Resolvers? Spring GraphQL provides `GraphQlTester` for writing expressive and readable tests. ```java // ArticleControllerTest.java @SpringBootTest @AutoConfigureGraphQlTester class ArticleControllerTest { @Autowired private GraphQlTester graphQlTester; @Test void shouldReturnArticleById() { // Given: GraphQL document 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: "New title", content: "Test content long enough to pass validation requirements", authorId: 1 }) { id title } } """; graphQlTester.document(mutation) .execute() .path("createArticle.id").entity(String.class).isNotEmpty() .path("createArticle.title").entity(String.class) .isEqualTo("New title"); } @Test void shouldReturnErrorForInvalidInput() { String mutation = """ mutation { createArticle(input: { title: "", content: "short", authorId: 1 }) { id } } """; graphQlTester.document(mutation) .execute() .errors() .expect(error -> error.getMessage() .contains("Title is required")); } @Test void shouldHandleVariables() { String query = """ query GetArticle($id: ID!) { article(id: $id) { id title } } """; graphQlTester.document(query) .variable("id", 42) .execute() .path("article").valueIsNull(); } } ``` ## How to Handle Real-Time Subscriptions? Subscriptions enable pushing real-time data to clients via 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)); } // Method called when an article is created public void publishArticle(Article article) { articleSink.tryEmitNext(article); } // Method called when a comment is added public void publishComment(Comment comment) { commentSink.tryEmitNext(comment); } } ``` WebSocket configuration requires an additional dependency: ```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(); } } ``` ## What Are GraphQL Security Best Practices? GraphQL security requires multiple protection layers against denial-of-service attacks and unauthorized access. ```java // SecurityConfig.java @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Protected GraphQL endpoint .requestMatchers("/graphql").authenticated() // GraphiQL accessible in dev only .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; } // Access restricted to authenticated users @QueryMapping @PreAuthorize("isAuthenticated()") public List
articles(@Argument int page, @Argument int size) { return articleRepository.findAll(PageRequest.of(page, size)) .getContent(); } // Only admins can delete @MutationMapping @PreAuthorize("hasRole('ADMIN')") public boolean deleteArticle(@Argument Long id) { articleRepository.deleteById(id); return true; } // Fine-grained check: only the author can modify @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(); } } ``` Protection against complex queries: ```yaml # application.yml spring: graphql: schema: introspection: # Disable introspection in production 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( "Query too complex: " + complexity + " (max: " + MAX_COMPLEXITY + ")" ); } return super.beginExecution(parameters, state); } } ``` ## Conclusion Spring for GraphQL provides elegant integration of GraphQL into the Spring ecosystem. Mastering resolvers, DataLoaders, and N+1 problem management forms the knowledge foundation expected in interviews. **Spring GraphQL Interview Checklist:** - ✅ Explain the role of `@QueryMapping`, `@MutationMapping`, and `@SchemaMapping` annotations - ✅ Describe the N+1 problem and its impact on GraphQL performance - ✅ Implement a DataLoader with `@BatchMapping` or BatchLoaderRegistry - ✅ Validate mutation inputs with Bean Validation - ✅ Secure resolvers with `@PreAuthorize` and Spring Security - ✅ Test resolvers with GraphQlTester - ✅ Configure subscriptions for real-time functionality - ✅ Protect the API against excessively complex queries --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1