Careeroza
Real World Example
Instagram-style Infinite Scroll with Keyset Pagination
OFFSET paging gets slower on deep pages: limit 100000,20 makes the database read and throw away 1 lakh rows. Feeds (Instagram, Twitter/X, Flipkart reviews) use keyset (seek) pagination: "give me 20 rows older than the last one I saw". ShopEase uses it for product reviews.
public interface ReviewRepository extends JpaRepository<Review, Long> {
// first page
List<ReviewView> findTop20ByProductIdOrderByIdDesc(Long productId);
// next pages: everything older than the last id the client saw
List<ReviewView> findTop20ByProductIdAndIdLessThanOrderByIdDesc(Long productId, Long lastSeenId);
}
@GetMapping("/api/products/{id}/reviews")
public ReviewPage reviews(@PathVariable Long id, @RequestParam(required = false) Long after) {
List<ReviewView> rows = (after == null)
? reviews.findTop20ByProductIdOrderByIdDesc(id)
: reviews.findTop20ByProductIdAndIdLessThanOrderByIdDesc(id, after);
Long next = rows.size() == 20 ? rows.get(19).getId() : null;
return new ReviewPage(rows, next);
}-- uses the index (product_id, id) and touches only 20 rows, however deep the page
select r1_0.id,r1_0.rating,r1_0.comment,r1_0.author_name
from reviews r1_0
where r1_0.product_id=? and r1_0.id<?
order by r1_0.id desc
limit ?Response
Example
curl "localhost:8080/api/products/7/reviews?after=88120"Example output
HTTP/1.1 200 OK
{"items":[{"id":88117,"rating":5,"comment":"Great bass for the price"},
{"id":88102,"rating":4,"comment":"Battery lasts 2 days"}],
"nextCursor":88051}| Approach | Deep page cost | Jump to page 57 | Use for |
|---|---|---|---|
OFFSET (Page) | grows with offset | yes | admin tables, search results |
OFFSET (Slice) | grows with offset | yes | small "load more" lists |
| Keyset / cursor | constant | no | feeds, reviews, activity logs |
Tip: Spring Data 3.1+ also offers ScrollPosition / Window<T> for keyset scrolling: Window<Review> findFirst20ByProductIdOrderByIdDesc(Long productId, ScrollPosition position).