You return a Spring Data Window<Book> from a resolver, yet the client receives edges, cursors, and pageInfo. What machinery produces that? Explain ConnectionFieldTypeVisitor and ConnectionAdapter.
answer
- TypeVisitor decorates the DataFetcher at build time
- detects Connection by ...Connection name
- ConnectionAdapter: Window/Slice -> edges + pageInfo
- cursor via ScrollPositionCursorStrategy (base64)
- Boot auto-wires it with Spring Data present
basics
~20 sA GraphQL type visitor, ConnectionFieldTypeVisitor, wraps the field's DataFetcher. When the resolver returns a Window (detected by a ConnectionAdapter), the visitor converts it into edges (node + cursor per item) and PageInfo, encoding each cursor from the item's ScrollPosition.
solid answer
~40 s`ConnectionFieldTypeVisitor` is a `GraphQLTypeVisitor` that runs at schema-build time. It finds every field whose GraphQL output type is a Relay Connection (by the `...Connection` naming convention) and decorates that field's `DataFetcher`. At runtime the decorated fetcher takes your raw return value — a Spring Data `Window`, `Slice`, or `Page` — and asks the registered `ConnectionAdapter`s which one supports it. The matching adapter (`WindowConnectionAdapter`, `SliceConnectionAdapter`) extracts the content list, computes `hasPreviousPage`/`hasNextPage`, and produces a cursor for each item via a `CursorStrategy` (default `ScrollPositionCursorStrategy`, which base64-encodes the item's `ScrollPosition`). The visitor then assembles the `Connection` map: `edges: [{ node, cursor }]` plus `pageInfo: { hasNextPage, hasPreviousPage, startCursor, endCursor }`. So you never hand-build edges — you just return a Window and Spring adapts it. In Spring Boot this is auto-configured when Spring Data is present.
code
java · 18 lines// Non-Boot / custom wiring of the output side.
// Boot does this automatically when Spring Data is on the classpath.
ScrollPositionCursorStrategy cursorStrategy =
new ScrollPositionCursorStrategy(); // base64-encoded cursors by default
ConnectionAdapter windowAdapter =
new WindowConnectionAdapter(cursorStrategy);
GraphQlSource source = GraphQlSource.schemaResourceBuilder()
.schemaResources(schemaResource)
.configureTypeVisitors(visitors ->
visitors.add(ConnectionFieldTypeVisitor.create(
List.of(windowAdapter))))
.build();
// Resolver just returns a Window — the visitor builds edges/pageInfo:
// @QueryMapping Window<Book> books(ScrollSubrange s) { ... }go deeper
Know that returning a Window is enough; Spring produces edges and pageInfo.
Name ConnectionFieldTypeVisitor and ConnectionAdapter and that cursors come from a CursorStrategy.
Explain build-time fetcher decoration, adapter selection, cursor encoding, and the Connection naming requirement.
Discuss extending it with a custom ConnectionAdapter/CursorStrategy and the Boot auto-config beans that assemble the pipeline.
**Big picture.** Spring for GraphQL splits paging into an *input* side (the `ScrollSubrange` argument) and an *output* side (adapting your returned container into a Relay `Connection`). This question is about the output side. **ConnectionFieldTypeVisitor.** A `graphql.schema.GraphQLTypeVisitor` visits the schema during `GraphQlSource` build. `ConnectionFieldTypeVisitor.create(List<ConnectionAdapter> adapters)` produces a visitor that, for each field whose output type is a Connection type (Relay names them `XxxConnection`), **wraps the field's existing `DataFetcher`** in a decorator. It is registered through `GraphQlSource.Builder#configureTypeVisitors`. **ConnectionAdapter.** `org.springframework.graphql.data.pagination.ConnectionAdapter` is a strategy: given a returned container object, `supports(Class)` says whether it can handle that type, and it exposes the content list, `hasPrevious`/`hasNext`, and — crucially — `cursorAt(container, index)` to derive each item's cursor. Spring Data ships adapters: - `WindowConnectionAdapter` for `org.springframework.data.domain.Window` (keyset or offset scroll; each item has a `ScrollPosition`). - `SliceConnectionAdapter` for `Slice`/`Page` (offset-based). Each adapter is built with a `CursorStrategy` to encode positions into opaque cursor strings. **CursorStrategy / ScrollPositionCursorStrategy.** `CursorStrategy<T>` encodes a `T` position to a String and back. `ScrollPositionCursorStrategy` (T = `ScrollPosition`) serializes keyset values or an offset, and wraps them with a `CursorEncoder` (default base64) so cursors are opaque. Decoding runs on the input side (the `after`/`before` args); encoding runs here on the output side. **Runtime flow for a request.** 1. Your `@QueryMapping Window<Book> books(ScrollSubrange s)` runs and returns a `Window<Book>`. 2. The decorated `DataFetcher` receives the `Window`, picks the `WindowConnectionAdapter` (via `supports`). 3. The adapter reads the content, and for each item computes a cursor from its `ScrollPosition` using `ScrollPositionCursorStrategy` → base64 string. 4. The visitor assembles `edges` (`{node, cursor}`) and `pageInfo` (`hasNextPage` from `window.hasNext()`, `hasPreviousPage` from paging state, `startCursor`/`endCursor` from the first/last edges). 5. GraphQL serializes that map against the `BookConnection`/`BookEdge`/`PageInfo` schema types. **Boot auto-configuration.** With `spring-boot-starter-graphql` + Spring Data on the classpath, Spring Boot registers a `ScrollPositionCursorStrategy`, the `Window`/`Slice` `ConnectionAdapter` beans, and a `GraphQlSourceBuilderCustomizer` that adds `ConnectionFieldTypeVisitor`. Without Boot you wire these yourself. **Gotchas.** - Detection is by the Connection type-name convention; if your schema type isn't recognized as a Connection, the visitor won't decorate it and you'll get a type mismatch (a `Window` won't serialize against a plain list). Follow Relay naming (`...Connection`, `...Edge`, `PageInfo`). - Return the container type an adapter supports (`Window`/`Slice`/`Page`); returning a plain `List` yields no cursors/pageInfo unless you add a custom adapter. - `hasNextPage` correctness relies on the repository fetching one extra row (Spring Data's `Window` does this) — don't reimplement it. - For a container type Spring doesn't know (e.g., a custom scroll result or a Neo4j type), implement a custom `ConnectionAdapter` and register it.
- Your Window is returned but the response fails to serialize as a Connection — likely cause?The schema field's output type isn't recognized as a Relay Connection (wrong naming, e.g. not `...Connection`), so ConnectionFieldTypeVisitor never decorated the fetcher and the raw Window can't map to the declared type.
- How would you paginate a container type Spring Data doesn't ship an adapter for?Implement a custom `ConnectionAdapter` (supports(), content, hasNext/hasPrevious, cursorAt) with a CursorStrategy, and register it in the list passed to `ConnectionFieldTypeVisitor.create(...)` (or as a bean under Boot).
saying these in an interview costs you the question
- Saying you must manually construct edges/pageInfo in the resolver
- Thinking the visitor runs per-request rather than decorating the fetcher at schema-build time
- Assuming any return type works — ignoring that an adapter must support the container
- Claiming cursors are generated client-side rather than by the CursorStrategy on the server