Spring Boot 4 & GraphQL: High-Performance APIs mit Java Records

Julian | Feb 20, 2026 min read

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 @Argument ohne 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.