Pagination — Pageable, Page, Slice, and Keyset
Fast recall
Quick revision
Core answer
LIMIT/OFFSET and SELECT COUNT(*); the count is often as expensive as the data on large tables. `SlicepageSize+1 rows — hasNext() is true if the extra row exists; ideal for 'Load More' / infinite scroll. `ListPageable works but loses metadata. For huge tables, `OFFSET n` itself is O(n) because the database scans and discards n rows; keyset (cursor) pagination stays constant-time. `@Query(countQuery="...")` lets you optimize the count separately. The `JOIN FETCH` + `Pageable` trap (HHH90003004) is the most common pagination bug — Hibernate paginates in memory.Whether the candidate knows the difference between Page and Slice, the COUNT cost, and keyset pagination
Always using `Page` when `Slice` would suffice — COUNT on a million-row table can dwarf the data query
Explain keyset pagination as an alternative to OFFSET for deep pagination, mention `countQuery` optimization, and the JOIN FETCH + Pageable trap
Interview-ready guide
Interview answer
Spring Data's Pageable handles page number, size, and sort. The Page vs Slice choice has a real performance impact on large tables.
`Page
Page— two queries: data with LIMIT/OFFSET andSELECT COUNT(*)for totalSlice— one query forpageSize+1rows;hasNext()is true if the extra row exists- On a 10M-row table the COUNT alone can take seconds — pick
Sliceunless total is required
Creating Pageable:
PageRequest.of(0, 20)— zero-indexedPageRequest.of(0, 20, Sort.by("createdAt").descending())— with sort- Spring MVC:
@PageableDefault(size = 20, sort = "createdAt", direction = DESC) Pageable pageableresolves from query params
The JOIN FETCH + Pageable trap:
- Fetching a
@OneToManywith JOIN FETCH and Pageable triggersHHH90003004 - Hibernate loads all matching rows in memory and applies pagination in Java — latent OOM
- Use
@EntityGraphinstead — pagination applied in SQL, associations fetched via IN-clause batching
Keyset pagination for scale:
- OFFSET 10000 forces a scan of 10000 rows — O(n)
WHERE (createdAt, id) < (:lastCreatedAt, :lastId) ORDER BY createdAt DESC, id DESC LIMIT 20is O(1)- Required for feeds, infinite scroll, and any mobile API serving deep pagination
Pagination — Pageable, Page, Slice, and Keyset
Direct Answer
Spring Data accepts Pageable as the last argument; the return type decides whether COUNT runs. Page<T> runs two queries — data with LIMIT/OFFSET and SELECT COUNT(*); the count is often as expensive as the data on large tables. Slice<T> runs one query for pageSize+1 rows — hasNext() is true if the extra row exists; ideal for 'Load More' / infinite scroll. List<T> with Pageable works but loses metadata. For huge tables, OFFSET n itself is O(n) because the database scans and discards n rows; keyset (cursor) pagination stays constant-time. @Query(countQuery="...") lets you optimize the count separately. The JOIN FETCH + Pageable trap (HHH90003004) is the most common pagination bug — Hibernate paginates in memory.
Common Mistakes & Pitfalls
Always using Page when Slice would suffice — COUNT on a million-row table can dwarf the data query
Check your understanding
Answer these without looking back at the article.
- Why does JOIN FETCH + Pageable fall back to in-memory pagination, and how does @EntityGraph fix it?
- When would you use Slice<T> over Page<T>, and what UI patterns map to each?
- How would you implement keyset pagination for a feed sorted by createdAt with ties?
- What's the right way to export 1M rows without Pageable or OFFSET?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.