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.

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.
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.
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.
# 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.
@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<Article> 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.
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 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
-- 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 queriesDeze 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.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Batch laadmethode
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Eén enkele query voor alle auteurs
List<Author> authors = authorRepository.findAllById(authorIds);
// Transformatie naar Map voor snelle lookup
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}De DataLoader registratie gebeurt via BatchLoaderRegistry in een dedicated configuratie.
@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:
@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();
}
// Geoptimaliseerde resolver met DataLoader
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> 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:
-- 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)Klaar om je Spring Boot gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Hoe een DataLoader implementeren met context?
Geavanceerde DataLoaders vereisen soms aanvullende context, zoals een tenant identifier voor multi-tenant applicaties of beveiligingsfilters.
@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) {
// GraphQL context ophalen
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Query gefilterd op 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);
}
}De context configuratie gebeurt via een WebGraphQL interceptor:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> 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 |
@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.
# 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:
@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;
}
}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
) {}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.
@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 voor auteurs - verwerkt alle artikelen tegelijk
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Unieke auteur ID's verzamelen
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Eén enkele query voor alle auteurs
Map<Long, Author> 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<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Batch ophalen van reacties
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Groepering per artikel
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 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.
@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();
}
}Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Hoe real-time subscriptions beheren?
Subscriptions maken het mogelijk om real-time data naar clients te pushen 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));
}
// 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:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@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.
@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();
}
}@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<Article> 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:
# application.yml
spring:
graphql:
schema:
introspection:
# Introspectie uitschakelen in productie
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 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,@MutationMappingen@SchemaMapping - ✅ Het N+1-probleem en de impact op GraphQL prestaties beschrijven
- ✅ Een DataLoader implementeren met
@BatchMappingof BatchLoaderRegistry - ✅ Mutation inputs valideren met Bean Validation
- ✅ Resolvers beveiligen met
@PreAuthorizeen Spring Security - ✅ Resolvers testen met GraphQlTester
- ✅ Subscriptions configureren voor real-time functionaliteit
- ✅ De API beschermen tegen te complexe queries
Tags
Delen
Gerelateerde artikelen

Spring Boot Sollicitatiegesprek: Transactiepropagatie
Beheers Spring Boot transactiepropagatie: REQUIRED, REQUIRES_NEW, NESTED en meer. 12 sollicitatievragen met code en veelgemaakte valkuilen.

Spring Data JPA N+1-Oplossingen in 2026: Fetch Join en EntityGraph
Volledige gids voor het detecteren en oplossen van het N+1-probleem in Spring Data JPA. Fetch join, @EntityGraph, batch fetching en strategieën voor queryprestaties.

30 Spring Boot Interviewvragen: Volledige Gids voor Java-ontwikkelaars
Bereid je voor op je Spring Boot-interviews met deze 30 essentiële vragen over auto-configuratie, starters, Spring Data JPA, security en testing.