Ch. 8 · Spring Boot

Spring Boot REST Pagination

Page results with Pageable, always sort stably, and avoid the deep-offset slowdown on large tables.

~2 min readintermediateupdated Oct 5, 2026

A list endpoint that returns everything breaks at scale, so it pages. Spring Data’s Pageable makes paging easy, but the details — a stable sort and how deep pages are fetched — decide whether the results are correct and fast.

Before you start

You should be comfortable with Spring Data repositories and REST controllers. This article covers paging and its pitfalls.

Step-by-step walkthrough

Step 1: Accept and bind Pageable

A controller method can take a Pageable parameter that Spring binds from page, size and sort query parameters. Return a Page or a stable DTO so the client gets the content plus the total and page metadata.

Step 2: Always apply a stable sort

Without a deterministic sort, rows with equal values can shift between pages, so a record appears twice or is skipped. Add a tiebreaker such as the id to the sort, so the ordering is total and paging is consistent.

Step 3: Watch the deep-offset cost

Offset paging is fine for the first pages, but a deep offset makes the database scan and discard many rows, which gets slow. For deep pagination use keyset paging, where the next page is fetched after the last item’s key, which stays fast.

Worked scenario

The endpoint binds paging and sorts by a stable key.

@GetMapping("/orders")
public Page<OrderDto> list(
    @RequestParam(required = false) String status,
    Pageable pageable) {
  Pageable stable = PageRequest.of(
      pageable.getPageNumber(), pageable.getPageSize(),
      Sort.by("createdAt").descending().and(Sort.by("id")));
  return repository.findByStatus(status, stable).map(OrderDto::from);
}
java

Walk through the example

The controller builds a Pageable with a total sort order, adding id as a tiebreaker so equal createdAt values keep a deterministic order across pages. Without it, a page boundary could repeat or skip a row. The DTO keeps the API shape decoupled from the entity.

Common mistake

Paging without a stable sort, which causes duplicates and missing rows across pages, or allowing a client to request a huge page size, which defeats the purpose. Another is deep offset paging on a large table.

Verify the behavior

Request consecutive pages and assert that no record appears twice or is skipped. Test with many equal sort values to stress the tiebreaker. Measure a deep page and compare with keyset paging on the same data.

Interview exercise

Why add an id to the sort when paging?

Answer and reasoning

Because page boundaries are computed by position, and if two rows compare equal, their order between pages is undefined without a tiebreaker. A row could appear on two pages or none. Adding a unique key makes the ordering total, so each row belongs to exactly one page.

Continue learning

Compare query design in Keyset pagination and JPA behavior in JPA N+1. Read the Spring Data web pagination documentation and try the Spring Boot interview questions.

More in Spring Boot

read ✓Spring Boot · mid

Spring Boot Problem Details for APIs

Return consistent RFC 7807 error responses, map exceptions centrally, and avoid leaking internals to clients.

~2 min readread →
read ✓Spring Boot · hard

Spring @Async and Executor Configuration

Run methods asynchronously with @Async, configure a bounded executor, and handle exceptions and the proxy boundary.

~2 min readread →
read ✓Spring Boot · hard

Spring Boot Caching Abstraction

Cache method results with @Cacheable, choose keys and TTLs, and evict on writes without the self-invocation trap.

~2 min readread →
esc