skip to content

How does the @BatchMapping annotation work in Spring for GraphQL, and what can the method's signature look like?

level: middleimportance: must knowfreq 48%

answer

  1. List<Source> in, Map<Source,Value> out
  2. typeName from list element, field from method name
  3. prefer Map over positional List
  4. Spring auto-registers a DataLoader
  5. one call per level, not per parent

basics

~20 s

@BatchMapping marks a controller method that resolves one field for a whole batch of parent objects at once. It receives a List of the parents and returns either a Map from parent to value or a List of values in the same order, so the field is loaded in a single call instead of one per parent.

solid answer

~40 s

@BatchMapping is the declarative fix for N+1 in Spring for GraphQL. Where @SchemaMapping resolves a field for one source object, @BatchMapping resolves the same field for a List<Source> — all parents at that level — in one method call. The type name is inferred from the List element type and the field name from the method name (overridable via @BatchMapping(typeName=…, field=…)). Return either a Map<Source, Value> keyed by the parent instance (order-independent), or a List<Value> positionally aligned with the input list. You can also return Mono/Flux for reactive flows. Under the hood Spring registers a DataLoader for this field, so the framework handles deferral and dispatch — you just write the batched fetch, e.g. authorRepository.findAllById(ids). Prefer the Map form; positional List is fragile if your query drops or reorders results.

code

java · 15 lines
java
@Controller
class BookController {
    private final AuthorRepository authors;
    BookController(AuthorRepository authors) { this.authors = authors; }

    // Resolves field 'author' on type 'Book' for ALL books at this level in ONE call
    @BatchMapping
    public Map<Book, Author> author(List<Book> books) {
        List<Long> ids = books.stream().map(Book::authorId).toList();
        Map<Long, Author> byId = authors.findAllById(ids).stream()
                .collect(Collectors.toMap(Author::id, a -> a));
        return books.stream()
                .collect(Collectors.toMap(b -> b, b -> byId.get(b.authorId())));
    }
}

go deeper

for a junior

Should know it batches a field for many parents at once and returns a Map or List.

for a middle

Must know the signature options, inference rules, and that a DataLoader is auto-registered.

for a senior

Explains Map-vs-List correlation risk, reactive returns, and when to drop to BatchLoaderRegistry.

for a principal

Weighs the declarative convenience against loss of DataLoader tuning and per-request scoping implications.

## What it is `@BatchMapping` is a controller-method annotation in Spring for GraphQL (`org.springframework.graphql.data.method.annotation.BatchMapping`) that resolves a **single field for a batch of parent (source) objects at once**, eliminating N+1 without you touching a DataLoader directly. Contrast with `@SchemaMapping` (and its shortcut for a parent, plain field mapping): that method is invoked **once per parent** and receives a single source object. `@BatchMapping` is invoked **once per level** and receives a `List` of **all** the parents. ## Inference rules - **Type name**: inferred from the **element type of the input `List`**. `List<Book> ` → the field belongs to GraphQL type `Book`. - **Field name**: inferred from the **method name**. A method `author(...)` resolves the `author` field. - Override either with `@BatchMapping(typeName = "Book", field = "writtenBy")`. ## Allowed return types 1. **`Map<Source, Value>`** — keyed by the **parent object instance**. Spring matches each parent to its value by map lookup. Order-independent and the **recommended** form. Requires your source type to have sensible `equals`/`hashCode`. 2. **`Collection<Value>` / `List<Value>`** — **positional**: element *i* of the output is the value for parent *i* of the input. Fragile — if the underlying query returns fewer rows or a different order, values are silently misaligned. 3. **Reactive wrappers**: `Mono<Map<Source, Value>>`, `Flux<Value>` for non-blocking data access. ## Method arguments - The `List<Source>` of parents (required). - Optionally `@ContextValue`, `GraphQLContext`, `BatchLoaderEnvironment`, `Locale`, etc. ## Example ```java @Controller class BookController { private final AuthorRepository authors; @BatchMapping // field "author" on type "Book" public Map<Book, Author> author(List<Book> books) { List<Long> ids = books.stream().map(Book::authorId).toList(); Map<Long, Author> byId = authors.findAllById(ids).stream() .collect(Collectors.toMap(Author::id, a -> a)); return books.stream() .collect(Collectors.toMap(b -> b, b -> byId.get(b.authorId()))); } } ``` ## How it avoids N+1 Spring registers a **DataLoader** for this field behind the scenes. During execution the engine defers each `Book.author` resolution, collects every `Book` at that level, then calls your `author(List<Book>)` method **once**. Two round trips total. ## Gotchas - **Prefer the `Map` return** — positional `List` breaks if the query filters/reorders. - Every parent must appear in the map/list; a missing entry surfaces as `null` for that field. - `@BatchMapping` is per-request scoped like any DataLoader — no cross-request caching. - It only batches **within one execution level**; it is not a substitute for pagination on large collections. - Because it hides the DataLoader, you lose fine control (custom cache, options) — drop to `BatchLoaderRegistry` when you need that.

  • Why is returning a Map<Book, Author> safer than returning a List<Author>?
    The Map is keyed by the parent instance, so Spring correlates each value explicitly. A List is positional — if your batch query returns rows in a different order or drops some, values silently misalign with the wrong parents.
  • Where do the typeName and field name come from if you don't specify them?
    typeName is inferred from the element type of the input List (List<Book> → Book), and the field name is inferred from the method name. Both can be overridden via @BatchMapping(typeName=…, field=…).

saying these in an interview costs you the question

  • Thinking @BatchMapping is called once per parent like @SchemaMapping
  • Assuming positional List return is as safe as the Map form
  • Believing you must manually register a DataLoader when using @BatchMapping

context