Während REST oft zu viel Daten liefert (Overfetching) oder gRPC für Web-Clients zu komplex in der Handhabung ist, hat sich GraphQL als der Standard für flexible Frontends etabliert. Mit Spring Boot 4 ist die Integration von spring-graphql so nahtlos wie nie zuvor. Vergiss manuelle Controller-Logik für jedes Feld – wir nutzen die Power von Declarative Schema Mapping.
Warum GraphQL in 2026?
In einer Welt von Micro-Frontends und mobilen Apps müssen wir dem Client die Kontrolle über die Datenstruktur geben. In Spring Boot 4 profitieren wir von:
- Auto-Configuration: Das Schema wird automatisch gegen deine Controller validiert.
- Virtual Threads: Jeder DataFetcher läuft standardmäßig effizient, auch bei blockierenden DB-Abfragen.
- Native Querydsl Integration: Komplexe Filterungen direkt aus dem GraphQL-Schema heraus.
Das Szenario: Task-Management API
Wir bauen eine API für ein Task-Board. Ein User möchte seine Tasks abrufen, aber nur die Titel und den Status sehen – ohne die kompletten Metadaten.
1. Das Schema: schema.graphqls
In Spring Boot 4 legen wir unser Schema unter src/main/resources/graphql/ ab.
type Query {
tasks: [Task]
taskById(id: ID!): Task
}
type Task {
id: ID
title: String
status: String
assignee: String
}
2. Die Dependencies (Maven)
Wie beim gRPC-Artikel bleiben wir bei Maven. Der neue Starter bündelt alles, was wir brauchen.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Implementierung der Controller: Records & Schema Mapping
In früheren Versionen mussten wir oft komplexe POJOs mit Boilerplate-Code schreiben. Mit Java 21+ und Spring Boot 4 nutzen wir Records für unsere Datenmodelle. Sie sind unveränderlich, prägnant und passen perfekt zur Struktur von GraphQL-Typen.
1. Das Datenmodell als Record
public record Task(String id, String title, String status, String assignee) {}
2. Der TaskController
In Spring Boot 4 brauchen wir keine speziellen Service-Beans mehr manuell zu registrieren, wenn wir die neuen Controller-Annotationen nutzen. @QueryMapping verbindet die Methode direkt mit dem gleichnamigen Feld in unserem Query-Typ im Schema.
@Controller
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@QueryMapping
public List<Task> tasks() {
// Dank Virtual Threads (spring.threads.virtual.enabled=true)
// können wir hier blockierende Repository-Aufrufe machen.
return taskService.findAllTasks();
}
@QueryMapping
public Task taskById(@Argument String id) {
return taskService.findTaskById(id);
}
}
Tip: Nutze
@Argumentohne expliziten Namen, wenn dein Java-Parameter exakt so heißt wie im GraphQL-Schema. Das spart Code und macht die API wartungsfreundlicher.
3. Effizientes Laden mit @SchemaMapping
Ein klassisches Problem bei GraphQL ist das N+1 Problem. Wenn wir für jeden Task den Assignee aus einem anderen Service laden müssten, wäre das extrem ineffizient. Spring Boot 4 optimiert dies durch verbesserte @BatchMapping Unterstützung, aber für einfache Fälle nutzen wir @SchemaMapping.
@SchemaMapping(typeName = "Task", field = "assignee")
public String getAssigneeName(Task task) {
// Diese Methode wird nur aufgerufen, wenn der Client das Feld 'assignee' anfragt.
return "User: " + task.assignee();
}
DataFetcher und das N+1 Problem lösen
Ein Standard-@SchemaMapping ist schnell geschrieben, führt aber bei Listen (wie unseren Tasks) dazu, dass für jeden Task eine separate Abfrage für den Assignee abgefeuert wird. In Spring Boot 4 lösen wir das elegant mit @BatchMapping.
1. Effizientes Batch Loading mit @BatchMapping
Statt für jeden Task einzeln die Datenbank oder einen externen Service anzufragen, sammeln wir die IDs und feuern einen Batch-Call ab. Spring nutzt hierfür intern den DataLoader.
@Controller
public class TaskController {
// Diese Methode ersetzt das einfache @SchemaMapping für 'assignee'
@BatchMapping
public Map<Task, String> assignee(List<Task> tasks) {
// Wir extrahieren alle User-IDs aus den Tasks
List<String> userIds = tasks.stream()
.map(Task::assigneeId)
.toList();
// Ein einziger Call an den User-Service / DB
Map<String, String> userNames = userService.getNamesByIds(userIds);
// Wir mappen die Ergebnisse zurück auf die Task-Objekte
return tasks.stream()
.collect(Collectors.toMap(
task -> task,
task -> userNames.getOrDefault(task.assigneeId(), "Unknown")
));
}
}
2. Custom Exception Handling
Nichts ist unprofessioneller als ein generischer Internal Server Error in der GraphQL-Response. In Spring Boot 4 nutzen wir den DataFetcherExceptionResolverAdapter, um technische Exceptions in saubere GraphQL-Errors zu übersetzen.
@Component
public class GraphQlExceptionResolver extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
if (ex instanceof TaskNotFoundException) {
return GraphQLError.newError()
.errorType(ErrorType.NOT_FOUND)
.message(ex.getMessage())
.path(env.getExecutionStepInfo().getPath())
.build();
}
return null;
}
}
Testing: Die API absichern
Damit wir sicherstellen, dass unsere DataFetcher auch das liefern, was sie sollen, nutzen wir den GraphQlTester. In Spring Boot 4 ist dieser extrem mächtig für Slice-Tests.
@GraphQlTest(TaskController.class)
class TaskControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldGetFirstTaskTitle() {
this.graphQlTester.document("{ tasks { title } }")
.execute()
.errors().verify()
.path("tasks[0].title")
.entity(String.class)
.isEqualTo("GraphQL Artikel schreiben");
}
}
Fazit: GraphQL in 2026
Mit Spring Boot 4 ist GraphQL endgültig erwachsen geworden. Die Kombination aus Java Records, Virtual Threads und dem BatchMapping erlaubt es uns, hochperformante und typsichere APIs zu bauen, ohne uns im N+1-Sumpf zu verlieren. Wenn du eine API baust, die flexibel von verschiedenen Frontends konsumiert werden soll, ist dieser Stack aktuell unschlagbar.
