Spring Boot Pagination and Sorting
A table with one million rows cannot travel to a browser in a single reply. The response would be huge, slow, and heavy on memory. Pagination splits the data into small pages, and sorting arranges rows in a useful order. Together they keep APIs fast and screens easy to read.
Spring Data JPA supports both features out of the box through the Pageable and Sort types.
The Book Pages Idea
A thick novel has hundreds of pages. Readers never swallow the whole book at once; they open one page at a time. A paginated API follows the same habit. The client asks for page 3 with 10 items, and the server returns exactly those items.
Database: 45 products Page 0 -> items 1 - 10 Page 1 -> items 11 - 20 Page 2 -> items 21 - 30 Page 3 -> items 31 - 40 Page 4 -> items 41 - 45 (last page, 5 items)
Page numbers start at zero in Spring Data. The first page is page 0.
Core Types
| Type | Purpose |
|---|---|
| Pageable | Holds the page number, page size, and sort order |
| PageRequest | Creates a Pageable object |
| Page | Holds the items plus paging details |
| Sort | Describes the sort fields and directions |
Repository Support
JpaRepository already carries paging methods. A Pageable argument works in custom methods as well.
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findByCategory(String category, Pageable pageable);
}Creating a Page Request
Pageable first = PageRequest.of(0, 10);
Pageable sorted = PageRequest.of(0, 10, Sort.by("price").descending());
Pageable multi = PageRequest.of(1, 5,
Sort.by("category").ascending().and(Sort.by("price").descending()));The first request returns the first ten items. The second sorts by price from high to low. The third sorts by category first and by price within each category.
Controller Example
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository repository;
public ProductController(ProductRepository repository) {
this.repository = repository;
}
@GetMapping
public Page<Product> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {
Sort sort = direction.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
return repository.findAll(PageRequest.of(page, size, sort));
}
}Clients control the result through the address.
GET /api/products?page=2&size=5&sortBy=price&direction=descAutomatic Pageable Binding
Spring MVC converts query parameters into a Pageable object when the method asks for one.
@GetMapping
public Page<Product> list(Pageable pageable) {
return repository.findAll(pageable);
}Clients call /api/products?page=0&size=5&sort=price,desc, and Spring fills the object without extra code.
Reading the Response
A Page converts to JSON with the items and helpful numbers.
{
"content": [ { "id": 11, "name": "Desk Lamp", "price": 24.0 } ],
"totalElements": 45,
"totalPages": 5,
"number": 1,
"size": 10,
"first": false,
"last": false
}| Field | Meaning |
|---|---|
| content | Items on the current page |
| totalElements | Count of all matching rows |
| totalPages | Number of pages available |
| number | Current page index |
| first / last | Position flags for navigation buttons |
Page, Slice, and List
A Page runs an extra count query to learn the total. A Slice skips the count and only reports whether a next slice exists. Choose Slice for endless scrolling feeds where totals do not matter, because it runs faster on large tables.
Protecting the API
- Cap the page size, for example at 100, so a client cannot request a million rows.
- Allow sorting only on approved field names. An unknown field name triggers an error.
- Add a database index on columns used for sorting.
- Choose a stable default sort, such as id, so pages never repeat or skip items.
int safeSize = Math.min(size, 100);
Pageable pageable = PageRequest.of(page, safeSize, sort);Page Numbers for Users
Users see page numbers starting at 1, while Spring Data starts at 0. Frontend code converts between the two, or the API accepts a one-based value and subtracts one before building the PageRequest. Sending the total page count in every reply lets the interface draw navigation buttons. Disabling the previous button on the first page and the next button on the last page prevents empty requests.
Best Practices
- Apply pagination to every list endpoint that can grow.
- Return DTO pages when entities hold private fields.
- Document paging parameters in the API documentation.
- Test the last page and an empty page.
Pagination and sorting keep response times steady as data grows. Users see organized pages, and servers carry a lighter load.
