How is the whole cursor-connection pipeline wired in Spring for GraphQL — the beans Boot auto-configures, how to customize the cursor encoding or page-size limits, and how to support a non-Spring-Data container?
answer
- input: SubrangeMethodArgumentResolver decodes cursor
- output: ConnectionFieldTypeVisitor.create(adapters)
- Boot auto-wires strategy + Window/Slice adapters + customizer
- override via @ConditionalOnMissingBean beans
- custom container -> implement ConnectionAdapter; clamp page size yourself
basics
~20 sWith Spring Data present, Boot auto-registers a ScrollPositionCursorStrategy, Window/Slice ConnectionAdapters, and a customizer that adds ConnectionFieldTypeVisitor. You customize by supplying your own CursorStrategy/CursorEncoder or ConnectionAdapter beans; for a foreign container, implement and register a custom ConnectionAdapter.
solid answer
~40 sThe pipeline has two halves sharing a `CursorStrategy`. Input: a `SubrangeMethodArgumentResolver` binds `ScrollSubrange`, decoding `after`/`before` via `ScrollPositionCursorStrategy`. Output: `ConnectionFieldTypeVisitor.create(adapters)`, added through `GraphQlSource.Builder#configureTypeVisitors`, decorates Connection-typed fetchers and uses `ConnectionAdapter`s (`WindowConnectionAdapter`, `SliceConnectionAdapter`) to build edges/pageInfo, encoding cursors with the same strategy. In Spring Boot, with `spring-graphql` + Spring Data on the classpath, all of this is auto-configured: it registers a `ScrollPositionCursorStrategy` (base64 `CursorEncoder`), the Window/Slice adapters, and a `GraphQlSourceBuilderCustomizer` that adds the visitor. To customize, define your own `CursorStrategy`/`CursorEncoder` bean (e.g. plaintext for debugging, or an encrypted encoder), or your own `ConnectionAdapter` beans. To page a non-Spring-Data container, implement `ConnectionAdapter` (supports/content/hasNext/hasPrevious/cursorAt) plus a matching `CursorStrategy` and register it. Enforce a max page size in the resolver.
code
java · 29 lines@Configuration
class GraphQlPaginationConfig {
// Override the default cursor encoding (e.g. readable cursors in dev,
// or swap base64() for a signed/encrypted encoder in prod).
@Bean
ScrollPositionCursorStrategy cursorStrategy() {
return new ScrollPositionCursorStrategy(CursorEncoder.base64());
}
// Custom adapter for a container Spring Data doesn't know about.
@Bean
ConnectionAdapter externalFeedAdapter(ScrollPositionCursorStrategy strategy) {
return new ExternalFeedConnectionAdapter(strategy);
}
}
@Controller
class FeedController {
private static final int MAX_PAGE = 100;
@QueryMapping
Window<Post> feed(ScrollSubrange subrange) {
int count = Math.min(subrange.count().orElse(20), MAX_PAGE); // clamp
ScrollPosition pos = subrange.position().orElse(ScrollPosition.keyset());
// ... repository.findBy(...).scroll(pos) with limit(count) ...
return service.load(pos, count, subrange.forward());
}
}go deeper
Know it's mostly automatic under Spring Boot with Spring Data — you just return a Window.
Name the two halves (Subrange resolver + ConnectionFieldTypeVisitor) sharing a CursorStrategy.
List the auto-configured beans and how @ConditionalOnMissingBean lets you override encoder/adapters.
Design the extension: custom ConnectionAdapter for foreign containers, signed cursors, app-wide CursorStrategy consistency, page-size and complexity limits.
**Two halves, one CursorStrategy.** - *Input half*: `org.springframework.graphql.data.method.annotation.support.SubrangeMethodArgumentResolver` (registered automatically) binds a `Subrange`/`ScrollSubrange` parameter. It uses a `CursorStrategy` to *decode* the incoming `after`/`before` cursor into a `ScrollPosition`, and reads `first`/`last` into the count and `forward` flag. - *Output half*: `ConnectionFieldTypeVisitor` (a `GraphQLTypeVisitor`) decorates each Connection-typed field's `DataFetcher`; the decorator delegates to a matching `ConnectionAdapter` to *encode* cursors and build `edges`/`pageInfo`. Both use the same `CursorStrategy<ScrollPosition>` so encode/decode are symmetric. **Beans Spring Boot auto-configures.** With `spring-boot-starter-graphql` and Spring Data (JPA/R2DBC/Mongo/etc.) on the classpath, Boot's GraphQL Data auto-configuration contributes: - a `ScrollPositionCursorStrategy` bean (built with a base64 `CursorEncoder` so cursors are opaque), - `WindowConnectionAdapter` and `SliceConnectionAdapter` beans (each wired with the cursor strategy), - a `GraphQlSourceBuilderCustomizer` that calls `configureTypeVisitors(v -> v.add(ConnectionFieldTypeVisitor.create(adapters)))`. Because these are conditional (`@ConditionalOnMissingBean`), you override any of them by declaring your own. **Customization points.** 1. *Cursor encoding.* Provide your own `CursorEncoder` (the default is `CursorEncoder.base64()`; `CursorEncoder.noOpEncoder()` yields readable cursors handy for debugging) or a fully custom `CursorStrategy`. Wrap it so both the resolver and adapters pick it up (declare the `ScrollPositionCursorStrategy`/`CursorStrategy` bean). Use this to add signing/encryption so cursors can't be tampered with. 2. *Page-size limits.* There's no magic global cap — enforce it yourself: read `subrange.count().orElse(DEFAULT)` and clamp to a `MAX_PAGE_SIZE` in the resolver (or a shared helper). This prevents a client requesting `first: 1000000`. 3. *Adapters.* Replace or add `ConnectionAdapter` beans to change how a container maps to a Connection. **Supporting a non-Spring-Data container.** If a data source returns its own paged type (a custom scroll result, a driver-specific type, a third-party client's page object), implement `org.springframework.graphql.data.pagination.ConnectionAdapter`: - `supports(Class<?>)` — true for your container type; - content accessor — the list of nodes; - `hasPrevious`/`hasNext` — boundary flags; - `cursorAt(container, index)` — produce the position/cursor for item i (via your `CursorStrategy`). Register it as a bean (Boot adds it to the adapter list) or pass it into `ConnectionFieldTypeVisitor.create(List.of(...))` when building the `GraphQlSource` manually. **Non-Boot manual wiring.** Build the `GraphQlSource` yourself: construct `ScrollPositionCursorStrategy`, the adapters, and `configureTypeVisitors(v -> v.add(ConnectionFieldTypeVisitor.create(adapters)))`; the `SubrangeMethodArgumentResolver` comes with the annotated-controller support. **Production/architecture concerns.** - *Cursor opacity & tamper-resistance*: base64 is opaque but not tamper-proof; if a forged cursor could leak data or crash decoding, sign/encrypt it via a custom encoder and fail closed on bad input. - *Max page size & complexity*: combine the clamp with query-complexity/`maxAggregatedListSize` limits to bound cost. - *totalCount*: expose only when needed; it forces a separate COUNT under keyset. - *Naming convention*: the visitor detects Connections by `...Connection` type names — keep the schema Relay-compliant or fields won't be decorated. - *Consistency*: use one CursorStrategy app-wide so cursors minted by one field can't be misread by another; version the encoding if you ever change it.
- You want cursors that clients cannot tamper with to probe other positions. How?Replace the default base64 CursorEncoder with a signing/encrypting encoder inside a custom ScrollPositionCursorStrategy (or CursorStrategy) bean; validate the signature on decode and fail closed on malformed cursors. Both input resolver and output adapters then use it consistently.
- There's no built-in global max page size. Where do you enforce it and why there?In the resolver/service, by clamping subrange.count() to a MAX before calling the repository — that's the single point where the client-supplied count enters the query, so it bounds DB cost. Pair with query-complexity limits.
- Which Spring Boot condition lets you override the auto-configured cursor strategy?The auto-config beans are @ConditionalOnMissingBean, so declaring your own ScrollPositionCursorStrategy / ConnectionAdapter bean replaces the default; Boot then wires your bean into the visitor and resolver.
saying these in an interview costs you the question
- Assuming Spring enforces a global max page size automatically
- Trying to configure cursor encoding on the visitor instead of via the shared CursorStrategy bean
- Thinking a custom container works without implementing a ConnectionAdapter
- Believing base64 cursors are tamper-proof / a security boundary
- Using different CursorStrategies for input decoding and output encoding, breaking round-trips