What is Relay-style cursor-based pagination in Spring for GraphQL, and what do the Connection, Edge, and PageInfo types represent?
answer
- Connection -> edges + pageInfo
- Edge = node + cursor
- first/after forward, last/before backward
- opaque cursor, don't parse
- return Window, Spring builds edges
basics
~20 sIt is a standard way to page a list using opaque cursors instead of page numbers. A Connection wraps Edges; each Edge has a node (the item) plus its cursor; PageInfo carries hasNextPage/hasPreviousPage and start/end cursors.
solid answer
~40 sRelay cursor connections are a GraphQL convention for stable pagination. A field returns a `XxxConnection` type containing `edges: [XxxEdge]` and `pageInfo: PageInfo`. Each `Edge` has `node` (the actual entity) and `cursor` (an opaque string pointing at that item's position). `PageInfo` exposes `hasNextPage`, `hasPreviousPage`, `startCursor`, and `endCursor`. Clients page forward with `first`/`after` and backward with `last`/`before`, passing back a cursor rather than an offset. Cursors are opaque — clients must not parse them. Spring for GraphQL supports this out of the box: you return a Spring Data `Window<T>`, and Spring auto-wraps it into the Connection/Edge/PageInfo shape and generates the cursors for you. The advantage over offset paging is stability: inserts/deletes elsewhere in the list don't shift or duplicate results.
code
java · 22 lines# GraphQL schema (SDL) — Relay connection shape
type Query {
books(first: Int, after: String, last: Int, before: String): BookConnection
}
type BookConnection {
edges: [BookEdge]
pageInfo: PageInfo
}
type BookEdge {
node: Book
cursor: String
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}go deeper
Know the vocabulary: Connection wraps Edges, Edge has node+cursor, PageInfo has hasNextPage and cursors; first/after vs last/before.
Know that Spring auto-generates edges/pageInfo when you return a Window, and cursors are opaque base64.
Explain why cursor paging beats offset for stability/performance and when totalCount is costly.
Frame connections as a contract decision — opacity enables server-side evolution of the position encoding without client breakage.
**The problem cursor pagination solves.** Classic offset pagination (`LIMIT 20 OFFSET 40`) is fragile: if a row is inserted or deleted before your current position between requests, items shift and you either skip or re-see rows. Cursor (a.k.a. keyset) pagination instead remembers *where you were* by the value of the sort key(s), so paging is stable under concurrent writes and is faster at deep offsets because the database can seek rather than count-and-skip. **The Relay Cursor Connections spec.** Relay standardized a wire shape that GraphQL servers commonly implement: - **Connection** — the return type of a paginated field, e.g. `BookConnection`. Contains `edges` and `pageInfo` (and optionally `totalCount`). - **Edge** — one entry: `{ node, cursor }`. `node` is the real domain object; `cursor` is an **opaque** base64 string that encodes the item's position (its keyset values or an offset). Opaque means the client treats it as a black box and just echoes it back. - **PageInfo** — `{ hasNextPage, hasPreviousPage, startCursor, endCursor }`. Lets a client know whether to keep paging and which cursor to send next. - **Pagination arguments** — `first: Int` + `after: String` for forward paging; `last: Int` + `before: String` for backward paging. **How Spring for GraphQL supports it.** In `spring-graphql` with Spring Data on the classpath, you don't hand-build edges. You: 1. Declare Relay types in your schema (the `...Connection`/`...Edge`/`PageInfo` types, named by convention ending in `Connection`). 2. Return a Spring Data `org.springframework.data.domain.Window<T>` (or `Slice`/`Page`) from your `@SchemaMapping`/`@QueryMapping` method. 3. Spring's `ConnectionFieldTypeVisitor` decorates the field's `DataFetcher`, detects the container via a registered `ConnectionAdapter`, and produces the `edges` (with per-item cursors) and `pageInfo` automatically. A `CursorStrategy` (default `ScrollPositionCursorStrategy`) encodes/decodes each cursor. **Key terms defined.** - *Cursor*: opaque token identifying a position in the ordered result. In Spring it encodes a Spring Data `ScrollPosition` (keyset values or an offset), base64-encoded. - *Keyset*: the tuple of sort-key values (e.g. `(createdAt, id)`) used to seek past the last-seen row. - *Window<T>*: Spring Data's scroll result — a list of items plus the `ScrollPosition` for each and `hasNext()`. **When to use.** Use cursor connections for large, frequently-changing, or infinitely-scrolled lists, and whenever a client needs stable forward/backward navigation. Offset paging is fine for small, stable, jump-to-page-N admin tables. **Gotchas.** Cursors are opaque — never let a client construct or interpret them. `totalCount` is optional and, with keyset paging, often expensive (a separate `COUNT`), so many APIs omit it. The sort order must be deterministic (unique) or cursors become ambiguous.
- Why are cursors opaque instead of, say, the raw id?So the server can change its internal position encoding (keyset values, offset, extra tie-breakers) without breaking clients, and so clients can't forge positions. Clients only ever echo a cursor back.
- Why is cursor paging more stable than offset paging?It anchors on the value of the last-seen row (a keyset seek) rather than counting N rows in. Inserts/deletes elsewhere don't shift the anchor, so you don't skip or duplicate rows.
saying these in an interview costs you the question
- Thinking a cursor is a page number or numeric offset the client increments
- Claiming you must manually build edges and pageInfo in the resolver
- Saying PageInfo is optional/omittable for Relay connections
- Believing totalCount is always present in a Connection