Keyset pagination resumes an ordered query after a cursor value instead of skipping an increasing number of earlier rows.
Spring Data keyset pagination: continue after the last stable identifier
The downloadable Spring source kit pins Java 21 and Spring Boot 4.0.8 with its managed dependencies. Run mvn test to check the named fixture.
Use a unique ordering key
The fixture orders stock summaries by their unique ascending id and returns at most two with findTop2ByIdGreaterThanOrderByIdAsc. Starting after zero yields one and two; starting after two yields three; starting after three yields an empty result. The next cursor comes from the final returned row, not the number of items seen across earlier pages.
This is a deliberately narrow keyset: one non-null unique identifier. Sorting by a repeated timestamp alone would need another tie-breaking key and a predicate that agrees with the entire ordering. Changing the sort direction without changing the cursor predicate produces missing or repeated records.
Do not imply a frozen snapshot
Separate requests can observe inserts, deletes and changed filters between pages. A monotonic id cursor avoids offset shifting but does not create snapshot isolation. If the product promises a fixed export, define a snapshot or watermark contract rather than describing all pagination as equivalent. Transaction visibility covers a different boundary.
The repository has a fixed limit of two to make the test inspectable. An HTTP endpoint needs validation for cursor syntax, range and authorization; if it encodes multiple cursor fields, reject ambiguous or tampered representations. Request bounds handles the earlier input layer.
Checked source
List<StockSummary> findTop2ByIdGreaterThanOrderByIdAsc(long afterId);
// First request: afterId = 0. Continue with the last returned id.
// The source-kit test verifies [1, 2], then [3], then [].Test the boundary
RepositoryQueryTest.cursorKeepsIdOrderAndMovesPastThePreviousLastId checks this contract in the source kit. Excerpts belong to the named classes; use the downloadable files for imports, configuration and assertions.
Costs and boundaries
This method materializes at most two projection rows per call. A suitable index can support seeking by id, but the fixture checks results, not a production query plan or throughput. Arbitrary sorting, total counts, concurrent export consistency and signed cursor encoding remain separate features.
Common Mistakes
- Include a unique tie-breaker when ordering values can repeat.
- Build the next cursor from the last returned row.
- Do not promise snapshot consistency across independent requests.
Read next
Spring Data JPA repositories: derive a query from mapped properties, Spring Data JPA projections: return selected fields without a full entity, Spring API pagination: bounded requests and immutable snapshots, Java JDBC isolation: uncommitted writes and separate sessions.
