# Spring GraphQL Sollicitatiegesprek: Resolvers, DataLoaders en Oplossingen voor het N+1-probleem > Voorbereiding op Spring GraphQL sollicitatiegesprekken met deze complete gids. Resolvers, DataLoaders, omgaan met het N+1-probleem, mutaties en best practices voor technische vragen. - 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 vereenvoudigt de integratie van GraphQL in Spring Boot applicaties. Deze technologie is onmisbaar geworden voor moderne API's, en sollicitatievragen over dit onderwerp komen steeds vaker voor. Deze gids behandelt de fundamentele concepten: resolvers, DataLoaders, het N+1-probleem en geavanceerde patterns. > **Focus van het gesprek** > > Recruiters testen vooral het begrip van het N+1-probleem en het gebruik van DataLoaders. Deze twee onderwerpen vertegenwoordigen 60% van de vragen in Spring GraphQL sollicitatiegesprekken. ## Wat is Spring for GraphQL? Spring for GraphQL is de officiële opvolger van GraphQL Java Spring, native geïntegreerd in het Spring framework sinds versie 2.7. Deze integratie biedt verschillende voordelen: ondersteuning voor Spring annotaties, integratie met Spring Security en transparant gebruik van WebFlux of MVC. De basisconfiguratie vereist alleen de afhankelijkheid `spring-boot-starter-graphql` en een GraphQL schema. ```java // build.gradle.kts dependencies { // Spring GraphQL starter met Spring MVC implementation("org.springframework.boot:spring-boot-starter-graphql") implementation("org.springframework.boot:spring-boot-starter-web") // Voor GraphQL tests testImplementation("org.springframework.graphql:spring-graphql-test") } ``` Het GraphQL schema bevindt zich in `src/main/resources/graphql/` met de extensie `.graphqls`. ```graphql # schema.graphqls type Query { # Een artikel ophalen via zijn identifier article(id: ID!): Article # Gepagineerde artikellijst articles(page: Int = 0, size: Int = 10): [Article!]! } type Article { id: ID! title: String! content: String! # Relatie met de auteur author: Author! # Lijst van reacties comments: [Comment!]! createdAt: String! } type Author { id: ID! name: String! email: String! # Artikelen geschreven door deze auteur articles: [Article!]! } type Comment { id: ID! content: String! author: Author! createdAt: String! } ``` Deze architectuur declareert types en hun relaties. GraphQL genereert automatisch de documentatie en valideert queries aan de serverkant. ## Hoe werken Spring GraphQL Resolvers? Resolvers vormen de kern van GraphQL-uitvoering. Elk schemaveld kan een dedicated resolver hebben. Spring gebruikt de annotatie `@QueryMapping` voor root queries en `@SchemaMapping` voor relaties. ```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 voor Query.article(id) @QueryMapping public Article article(@Argument Long id) { return articleRepository.findById(id) .orElseThrow(() -> new ArticleNotFoundException(id)); } // Resolver voor 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 voor Article.author - aangeroepen voor elk artikel @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return authorRepository.findById(article.getAuthorId()) .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId())); } } ``` De `author` resolver wordt voor elk geretourneerd artikel uitgevoerd. Deze flexibele architectuur laadt data on-demand maar introduceert het N+1-probleem. > **Klassieke valkuil bij sollicitatiegesprekken** > > Een kandidaat die een `@SchemaMapping` resolver implementeert zonder het N+1-probleem te noemen, toont onvolledig GraphQL begrip. Recruiters verwachten deze analyse systematisch. ## Wat is het N+1-probleem in GraphQL? Het N+1-probleem ontstaat wanneer een GraphQL query N extra queries activeert om de relaties te laden. Dit destructieve patroon doet zich systematisch voor bij basis resolvers. Beschouw een query die 50 artikelen met hun auteurs ophaalt: ```graphql # GraphQL query query { articles(size: 50) { id title author { name } } } ``` Met de vorige resolver voert deze query uit: - 1 query voor de 50 artikelen - 50 queries om elke auteur individueel te laden ```sql -- Query 1: artikelen ophalen SELECT * FROM articles LIMIT 50 -- Queries 2-51: elke auteur individueel SELECT * FROM authors WHERE id = 1 SELECT * FROM authors WHERE id = 2 SELECT * FROM authors WHERE id = 3 -- ... 47 extra queries ``` Deze vermenigvuldiging van queries verslechtert de prestaties drastisch. Een endpoint dat in 50ms reageert kan met N+1 oplopen tot 2 seconden. ## Hoe werken DataLoaders? DataLoaders bundelen individuele queries in batchverzoeken. In plaats van elke auteur afzonderlijk te laden, verzamelt de DataLoader alle gevraagde ID's en voert één enkele query uit. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Batch laadmethode public Mono> loadAuthors(Set authorIds) { // Eén enkele query voor alle auteurs List authors = authorRepository.findAllById(authorIds); // Transformatie naar Map voor snelle lookup Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` De DataLoader registratie gebeurt via `BatchLoaderRegistry` in een dedicated configuratie. ```java // GraphQLConfig.java @Configuration public class GraphQLConfig { @Bean public BatchLoaderRegistry batchLoaderRegistry( AuthorBatchLoader authorBatchLoader) { return registry -> { // Registratie van DataLoader voor auteurs registry.forTypePair(Long.class, Author.class) .registerMappedBatchLoader((authorIds, env) -> authorBatchLoader.loadAuthors(authorIds)); }; } } ``` De gewijzigde resolver gebruikt nu de DataLoader in plaats van directe toegang: ```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(); } // Geoptimaliseerde resolver met DataLoader @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader authorDataLoader) { // De DataLoader bundelt automatisch de aanroepen return authorDataLoader.load(article.getAuthorId()); } } ``` Met deze aanpak voert dezelfde query voor 50 artikelen slechts 2 SQL queries uit: ```sql -- Query 1: artikelen ophalen SELECT * FROM articles LIMIT 50 -- Query 2: alle auteurs in één keer SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50) ``` ## Hoe een DataLoader implementeren met context? Geavanceerde DataLoaders vereisen soms aanvullende context, zoals een tenant identifier voor multi-tenant applicaties of beveiligingsfilters. ```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) { // GraphQL context ophalen GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Query gefilterd op tenant List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` De context configuratie gebeurt via een WebGraphQL interceptor: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono intercept( WebGraphQlRequest request, Chain chain) { // Tenant uit headers extraheren String tenantId = request.getHeaders() .getFirst("X-Tenant-ID"); // Toevoegen aan GraphQL context request.configureExecutionInput((input, builder) -> builder.graphQLContext(ctx -> ctx.put("tenantId", tenantId) ).build() ); return chain.next(request); } } ``` ## Wat zijn de verschillen tussen @QueryMapping en @SchemaMapping? Deze klassieke sollicitatievraag verifieert het begrip van de resolver hiërarchie. | Annotatie | Gebruik | Equivalent | |-----------|---------|------------| | `@QueryMapping` | Root velden van type Query | `@SchemaMapping(typeName = "Query")` | | `@MutationMapping` | Root velden van type Mutation | `@SchemaMapping(typeName = "Mutation")` | | `@SubscriptionMapping` | Real-time subscriptions | `@SchemaMapping(typeName = "Subscription")` | | `@SchemaMapping` | Alle velden van elk type | Generieke vorm | ```java // EquivalenceDemo.java @Controller public class EquivalenceDemo { // Deze twee declaraties zijn 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); } // Voor geneste velden werkt alleen @SchemaMapping @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return findAuthor(article.getAuthorId()); } // Alternatieve syntaxis met type als parameter @SchemaMapping public Author author(Article article) { // Spring leidt typeName = "Article" af van de parameter return findAuthor(article.getAuthorId()); } } ``` ## Hoe mutaties beheren met validatie? GraphQL mutaties wijzigen data. Spring GraphQL integreert met Bean Validation om inputs te valideren. ```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 } ``` De controller gebruikt `@Valid` om de validatie te activeren: ```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) { // Validatie wordt automatisch uitgevoerd 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 = "Titel is verplicht") @Size(min = 5, max = 200, message = "Titel moet tussen 5 en 200 tekens zijn") String title, @NotBlank(message = "Inhoud is verplicht") @Size(min = 100, message = "Inhoud moet minstens 100 tekens zijn") String content, @NotNull(message = "Auteur is verplicht") Long authorId ) {} ``` > **Foutafhandeling** > > Validatiefouten retourneren automatisch een gestructureerde GraphQL-fout met het pad van het ongeldige veld. Spring GraphQL formatteert deze fouten volgens de GraphQL specificatie. ## Hoe queries optimaliseren met @BatchMapping? De annotatie `@BatchMapping` vereenvoudigt het maken van DataLoaders direct in de controllers. Deze aanpak vermijdt expliciete BatchLoaderRegistry configuratie. ```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 voor auteurs - verwerkt alle artikelen tegelijk @BatchMapping public Map author(List
articles) { // Unieke auteur ID's verzamelen Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Eén enkele query voor alle auteurs Map authorsById = authorRepository .findAllById(authorIds) .stream() .collect(Collectors.toMap(Author::getId, Function.identity())); // Associatie artikel -> auteur return articles.stream() .collect(Collectors.toMap( Function.identity(), article -> authorsById.get(article.getAuthorId()) )); } // BatchMapping voor reacties @BatchMapping public Map> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Batch ophalen van reacties List allComments = commentRepository .findByArticleIdIn(articleIds); // Groepering per artikel 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` biedt een beknoptere syntaxis dan handmatige DataLoader configuratie, met dezelfde performance garanties. ## Hoe GraphQL Resolvers testen? Spring GraphQL biedt `GraphQlTester` om expressieve en leesbare tests te schrijven. ```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: "Nieuwe titel", content: "Test inhoud lang genoeg om de validatiebeperkingen te halen", authorId: 1 }) { id title } } """; graphQlTester.document(mutation) .execute() .path("createArticle.id").entity(String.class).isNotEmpty() .path("createArticle.title").entity(String.class) .isEqualTo("Nieuwe titel"); } @Test void shouldReturnErrorForInvalidInput() { String mutation = """ mutation { createArticle(input: { title: "", content: "kort", authorId: 1 }) { id } } """; graphQlTester.document(mutation) .execute() .errors() .expect(error -> error.getMessage() .contains("Titel is verplicht")); } @Test void shouldHandleVariables() { String query = """ query GetArticle($id: ID!) { article(id: $id) { id title } } """; graphQlTester.document(query) .variable("id", 42) .execute() .path("article").valueIsNull(); } } ``` ## Hoe real-time subscriptions beheren? Subscriptions maken het mogelijk om real-time data naar clients te pushen 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)); } // Methode aangeroepen bij het maken van een artikel public void publishArticle(Article article) { articleSink.tryEmitNext(article); } // Methode aangeroepen bij het toevoegen van een reactie public void publishComment(Comment comment) { commentSink.tryEmitNext(comment); } } ``` De WebSocket configuratie vereist een aanvullende afhankelijkheid: ```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(); } } ``` ## Wat zijn de best practices voor GraphQL beveiliging? GraphQL beveiliging vereist meerdere beschermingslagen tegen denial-of-service aanvallen en ongeautoriseerde toegang. ```java // SecurityConfig.java @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Beveiligd GraphQL endpoint .requestMatchers("/graphql").authenticated() // GraphiQL alleen toegankelijk in 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; } // Toegang beperkt tot geauthenticeerde gebruikers @QueryMapping @PreAuthorize("isAuthenticated()") public List
articles(@Argument int page, @Argument int size) { return articleRepository.findAll(PageRequest.of(page, size)) .getContent(); } // Alleen administrators kunnen verwijderen @MutationMapping @PreAuthorize("hasRole('ADMIN')") public boolean deleteArticle(@Argument Long id) { articleRepository.deleteById(id); return true; } // Fijnmazige controle: alleen de auteur kan wijzigen @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(); } } ``` Bescherming tegen complexe queries: ```yaml # application.yml spring: graphql: schema: introspection: # Introspectie uitschakelen in productie 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 te complex: " + complexity + " (max: " + MAX_COMPLEXITY + ")" ); } return super.beginExecution(parameters, state); } } ``` ## Conclusie Spring for GraphQL biedt een elegante GraphQL integratie in het Spring ecosysteem. De beheersing van resolvers, DataLoaders en het beheer van het N+1-probleem vormen de kennisbasis die wordt verwacht bij sollicitatiegesprekken. **Spring GraphQL sollicitatie checklist:** - ✅ De rol uitleggen van annotaties `@QueryMapping`, `@MutationMapping` en `@SchemaMapping` - ✅ Het N+1-probleem en de impact op GraphQL prestaties beschrijven - ✅ Een DataLoader implementeren met `@BatchMapping` of BatchLoaderRegistry - ✅ Mutation inputs valideren met Bean Validation - ✅ Resolvers beveiligen met `@PreAuthorize` en Spring Security - ✅ Resolvers testen met GraphQlTester - ✅ Subscriptions configureren voor real-time functionaliteit - ✅ De API beschermen tegen te complexe queries --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1