Loading all records from a database table in a single query is impractical for large datasets – it consumes excessive memory, slows down the application, and degrades user experience.
Spring Data JPA provides first‑class pagination and sorting support through the Pageable interface and Page return type – no manual SQL LIMIT/OFFSET needed.
In this guide, we’ll walk through everything you need to know to implement efficient, production‑ready pagination in your Spring Boot applications.
The Key Classes
Spring Data JPA’s pagination API revolves around these core types:
| Class / Interface |
Role |
Pageable
|
Input – describes the page number, page size, and sort order to fetch |
PageRequest
|
Concrete implementation of Pageable; used to construct page requests |
Sort
|
Describes sort direction and properties |
Page<T>
|
Output – contains the page data plus metadata (total elements, total pages, etc.) |
Slice<T>
|
Output – lighter than Page; contains data + hasNext flag, no total count query |
Repository Setup
Extend JpaRepository (which already extends PagingAndSortingRepository) – pagination is inherited automatically:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface ProductRepository extends JpaRepository<Product, Long> {
// findAll(Pageable) is inherited from PagingAndSortingRepository
// Derived query method – also accepts Pageable
Page<Product> findByCategory(String category, Pageable pageable);
// Custom JPQL with pagination
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
Page<Product> findAffordable(@Param("maxPrice") BigDecimal maxPrice, Pageable pageable);
}
Spring Data automatically generates the LIMIT/OFFSET SQL (or the equivalent for your database) and a count query for Page return types.
You can also use @Query with native SQL if needed, but the JPQL version is database‑independent.
Creating a PageRequest
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
// Page 0 (first page), 10 results, no sort
Pageable pageable = PageRequest.of(0, 10);
// Page 2 (third page), 5 results, sorted by name ascending
Pageable sorted = PageRequest.of(2, 5, Sort.by("name"));
// Multiple sort fields
Pageable multiSort = PageRequest.of(0, 20,
Sort.by(Sort.Direction.DESC, "createdAt")
.and(Sort.by(Sort.Direction.ASC, "name")));
// Use the pageable to query the repository
Page<Product> page = productRepository.findAll(pageable);
Important: Page numbers are zero‑based: page 0 is the first page, page 1 is the second, and so on.
This matches the convention used in Spring Data and most REST APIs.
What Page<T> Contains
Page<Product> page = productRepository.findAll(PageRequest.of(0, 10));
page.getContent(); // List<Product> – the actual data
page.getTotalElements(); // long – total records in database
page.getTotalPages(); // int – ceil(totalElements / pageSize)
page.getNumber(); // int – current page number (0-based)
page.getSize(); // int – page size requested
page.getNumberOfElements(); // int – actual elements in this page (may be less on last page)
page.isFirst(); // boolean
page.isLast(); // boolean
page.hasNext(); // boolean
page.hasPrevious(); // boolean
REST Controller with Pagination
Expose pagination as query parameters in your REST endpoint. Spring MVC can bind Pageable directly from request parameters when @EnableSpringDataWebSupport is active (enabled automatically with Spring Boot).
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository repository;
public ProductController(ProductRepository repository) {
this.repository = repository;
}
@GetMapping
public Page<Product> getProducts(
// @PageableDefault sets fallback values if parameters are not provided
@PageableDefault(size = 20, sort = "name") Pageable pageable
) {
return repository.findAll(pageable);
}
@GetMapping("/category/{category}")
public Page<Product> getByCategory(
@PathVariable String category,
@PageableDefault(size = 10) Pageable pageable
) {
return repository.findByCategory(category, pageable);
}
}
The endpoint now accepts these query parameters:
page – page number (0‑based, default 0)
size – page size (default from @PageableDefault)
sort – sort field and direction, e.g. sort=name,desc
Example request: GET /api/products?page=1&size=10&sort=price,desc
You can also omit @PageableDefault and rely on global defaults by setting spring.data.web.pageable.default-page-size and spring.data.web.pageable.max-page-size in your application.properties.
JSON Response Structure
When you return Page<T> from a Spring MVC controller, it serializes to JSON with both content and metadata:
{
"content": [
{ "id": 11, "name": "Keyboard", "price": 49.99 },
{ "id": 12, "name": "Mouse", "price": 29.99 }
],
"pageable": {
"sort": { "sorted": true, "unsorted": false },
"pageNumber": 1,
"pageSize": 10,
"offset": 10
},
"totalPages": 5,
"totalElements": 47,
"last": false,
"first": false,
"numberOfElements": 10,
"size": 10,
"number": 1,
"empty": false
}
This structure is compatible with most front‑end pagination components (e.g., Angular Material, React Table).
Page vs Slice
Spring Data JPA offers two main return types for paginated queries. Here’s how they compare:
| Return Type |
Runs Count Query? |
Has Total Pages/Elements? |
Best for |
Page<T> |
✅ Yes |
✅ Yes |
Traditional pagination with page numbers ("Page 3 of 12") |
Slice<T> |
❌ No (only checks if next page exists) |
❌ No (only hasNext) |
Infinite scroll / "Load more" UIs; avoids expensive COUNT query |
List<T> |
❌ No |
❌ No |
When you want data but handle pagination yourself (not recommended) |
For large tables, the COUNT(*) query that Page requires can be expensive. If you're building an infinite scroll UI where you only need to know if there are more results, Slice is more efficient.
To use Slice, simply change your repository method return type to Slice<T> – Spring Data will automatically generate the appropriate query without the count.
Sorting Without Pagination
You can also use Sort independently of Pageable when you need sorted results but don't need pagination:
// Repository method with Sort parameter
List<Product> findByCategory(String category, Sort sort);
// Usage:
Sort sort = Sort.by(Sort.Direction.ASC, "name");
List<Product> products = repository.findByCategory("Electronics", sort);
Performance Considerations
- Large offsets: When you request a high page number (e.g., page 1000 with size 20), the generated SQL uses
OFFSET 20000. This can be slow on large tables because the database still has to scan rows up to that offset. Consider using keyset pagination (also called “seek method”) if you need to navigate deeply. Spring Data doesn't support keyset pagination out‑of‑the‑box, but you can implement it with custom queries using WHERE id > :lastId.
- Count queries: The
COUNT(*) query for Page can become expensive on tables with millions of rows. If you have a very large table and don't need exact total counts, use Slice instead.
- Sorting on unindexed columns: Sorting on columns without database indexes will force a full table scan. Always ensure that the columns you sort on are indexed, especially for large datasets.
- Default page size: Set a reasonable default page size (e.g., 20 or 50) to avoid accidentally loading too many records at once. Use
@PageableDefault or global properties to enforce a maximum size.
Sorting with Multiple Fields and Null Handling
You can control how null values are sorted using the Sort.Order class:
Sort sort = Sort.by(
Sort.Order.asc("name").nullsFirst(),
Sort.Order.desc("createdAt").nullsLast()
);
Pageable pageable = PageRequest.of(0, 10, sort);
This is useful when you want to place nulls at the beginning or end of the result set.
Using Pagination with Projections (DTOs)
If you want to fetch only a subset of columns (e.g., for performance), you can use DTO projections with pagination:
public interface ProductSummary {
String getName();
BigDecimal getPrice();
}
// Repository method
Page<ProductSummary> findByCategory(String category, Pageable pageable);
Spring Data will generate a query that selects only the needed columns, and the pagination works the same way as with entities.
Best Practices
- Always set a maximum page size – use
@PageableDefault with maxPageSize or configure spring.data.web.pageable.max-page-size to prevent clients from requesting huge pages that could overload your database.
- Use
Slice for infinite scroll – avoid the count query cost when you don’t need total page numbers.
- Index your sort columns – ensure that all columns used in
Sort.by() have database indexes.
- Validate sort parameters – to prevent malicious sort fields, consider whitelisting allowed sort properties (e.g., by checking the field name against a set of known column names).
- Prefer default values – always specify defaults for page, size, and sort so that your API works without any parameters.
- Use
@PageableDefault with sort and direction for a clean and consistent API.
Common Pitfalls and Troubleshooting
- Page is 0‑based – many developers expect 1‑based pages. Make sure your front‑end sends
page=0 for the first page, or you can create a custom Pageable resolver to convert from 1‑based.
- Missing
@EnableSpringDataWebSupport – in Spring Boot, it’s auto‑configured. In plain Spring MVC, you need to add it to a configuration class.
- Sort parameter format – the query parameter should be like
sort=name,desc (note: comma, not space). Multiple sorts: sort=name,desc&sort=id,asc.
- Large offset performance – if you're experiencing slow queries on large offsets, consider implementing keyset pagination or using
@Query with a native WHERE clause that uses the last retrieved ID.
- Count query fails – when using complex joins or distinct queries, the count query may fail. You can provide a custom count query with
@Query(countQuery = "...").
Summary
Spring Data JPA's pagination is built around three components:
PageRequest.of(page, size, sort) to specify what you want, repository methods that accept Pageable and return Page<T>, and the Page object that carries both the data and metadata (total elements, total pages, current page).
In a REST controller, declare Pageable as a method parameter and Spring MVC automatically binds it from page, size, and sort query parameters.
Use Slice<T> instead of Page<T> when you want to avoid the count query on very large tables, and always protect your endpoints with sensible defaults and maximum page sizes.
With these tools, you can build efficient, user‑friendly APIs that handle large datasets gracefully.
Happy coding!