Spring Boot Pagination with Spring Data JPA: A Complete End-to-End Guide
When building REST APIs, returning all records from a database table is rarely a good idea. Imagine a Product table with 10 million records. Returning every record inβ¦
When building REST APIs, returning all records from a database table is rarely a good idea. Imagine a Product table with 10 million records. Returning every record in a single API response would consume excessive memory, increase response time, and degrade the user experience.
This is where pagination comes in.
Pagination allows clients to request only a subset of records at a time. Instead of returning all products, the API can return 10 or 20 records per request, along with metadata such as the current page number, total pages, and total records.
Spring Data JPA makes pagination extremely easy through the Page, Pageable, and PageRequest classes.
In this article, we’ll implement pagination step by step and understand what happens internally.
Why Do We Need Pagination?
Suppose your database contains 100,000 products.
Without pagination:
GET /v1/products
The API would return all 100,000 products.
Problems:
- High memory consumption
- Slow API response
- Increased network traffic
- Poor frontend performance
- Bad user experience
Instead, clients can request only the required data.
Example:
GET /v1/products?page=0&size=10
This returns only the first 10 products.
Pagination Parameters
Most REST APIs accept the following query parameters.
| Parameter | Description | Example |
|---|---|---|
| page | Current page number (0-based) | 0 |
| size | Number of records per page | 10 |
| sortBy | Column used for sorting | price |
| direction | Sorting order | asc / desc |
Example request:
GET /v1/products?page=0&size=10&sortBy=price&direction=desc
Meaning:
- Return the first page
- Maximum 10 products
- Sort by price
- Highest price first
Project Structure
controller
ProductController
service
ProductService
ProductServiceImpl
repository
ProductRepository
entity
Product
dto
ProductResponse
PaginationResponse
ApiResponse
Step 1: Repository
One of the biggest advantages of Spring Data JPA is that pagination support is already built into JpaRepository.
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
No additional method is required.
Internally, Spring provides methods like:
Page<T> findAll(Pageable pageable);
Step 2: Service Interface
public interface ProductService {
ApiResponse<PaginationResponse<ProductResponse>> getAllProducts(
int page,
int size,
String sortBy,
String direction);
}
The service receives all pagination parameters from the controller.
Step 3: Controller
@GetMapping
public ResponseEntity<ApiResponse<PaginationResponse<ProductResponse>>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {
return ResponseEntity.ok(
productService.getAllProducts(
page,
size,
sortBy,
direction));
}
Understanding @RequestParam
If the client doesn’t pass any values,
GET /v1/products
Spring automatically assigns
page = 0
size = 10
sortBy = id
direction = asc
Step 4: Creating the Sort Object
The first step inside the service is creating the sorting instruction.
Sort sort = direction.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Suppose the client calls
GET /v1/products?sortBy=price&direction=desc
Spring receives
sortBy = "price";
direction = "desc";
Since direction is “desc”, Spring creates
Sort.by("price").descending();
This object represents
ORDER BY price DESC
Notice that no SQL is executed here.
The Sort object simply stores the sorting instruction.
Understanding sortBy vs direction
Many beginners confuse these two parameters.
sortBy
Specifies which column should be used for sorting.
Examples:
name
price
createdAt
quantity
direction
Specifies how the records should be sorted.
asc
desc
Example:
GET /products?sortBy=name&direction=asc
Result
Apple
Laptop
Mouse
Phone
Another example
GET /products?sortBy=price&direction=desc
Result
75000
45000
1200
500
Step 5: Creating Pageable
Now we combine pagination and sorting.
Pageable pageable = PageRequest.of(page, size, sort);
Suppose
page = 1
size = 5
sort = price DESC
Spring creates a Pageable object containing
Page Number : 1
Page Size : 5
Sort By : price
Direction : DESC
Still, no SQL query has been executed.
The Pageable object is simply carrying pagination information.
What Does Page Size Mean?
Suppose your table has 12 products.
1 Laptop
2 Mouse
3 Keyboard
4 Phone
5 Monitor
6 Tablet
7 Printer
8 Camera
9 SSD
10 Speaker
11 Hard Disk
12 Webcam
If
GET /products?page=0&size=5
Output
Laptop
Mouse
Keyboard
Phone
Monitor
Only five records are returned.
Next page
GET /products?page=1&size=5
Output
Tablet
Printer
Camera
SSD
Speaker
Third page
GET /products?page=2&size=5
Output
Hard Disk
Webcam
Only two records remain.
So
Page Size = Maximum number of records returned per page.
It does not mean the database contains only five records.
Step 6: Fetching Data
Page<Product> productPage = repository.findAll(pageable);
This is the point where SQL is generated.
Suppose
page = 1
size = 5
sort = price DESC
Spring generates SQL similar to
SELECT *
FROM product
ORDER BY price DESC
LIMIT 5 OFFSET 5;
Understanding LIMIT
LIMIT 5
means
Return only five records.
Understanding OFFSET
OFFSET 5
means
Skip the first five records.
Formula
Offset = Page Number Γ Page Size
Examples
| Page | Size | Offset |
|---|---|---|
| 0 | 10 | 0 |
| 1 | 10 | 10 |
| 2 | 10 | 20 |
| 3 | 10 | 30 |
What is Page?
Most beginners think findAll(pageable) returns only a List.
It doesn’t.
It returns a Page.
Page<Product> productPage
The Page object contains
- List of products
- Current page
- Page size
- Total records
- Total pages
- Whether it is the first page
- Whether it is the last page
- Whether next page exists
- Whether previous page exists
Example
Page
content
--------
Laptop
Phone
Mouse
page = 0
size = 3
totalElements = 12
totalPages = 4
first = true
last = false
Step 7: Convert Entity to DTO
Never expose entities directly.
Instead,
Page<ProductResponse> response =
productPage.map(this::mapToResponse);
Spring internally converts every Product into ProductResponse.
Equivalent code
List<ProductResponse> responses = new ArrayList<>();
for(Product product : productPage.getContent()){
responses.add(mapToResponse(product));
}
The beauty of map() is that it preserves all pagination metadata.
Only the object type changes.
Page<Product>
β
Page<ProductResponse>
Everything else remains unchanged.
Custom Pagination Response
Returning Spring’s Page object exposes unnecessary internal details.
Instead create
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PaginationResponse<T>{
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean first;
private boolean last;
private boolean hasNext;
private boolean hasPrevious;
}
This gives complete control over the API response.
Sample API Request
GET /v1/products?page=0&size=5&sortBy=price&direction=desc
Sample Response
{
"success": true,
"message": "Products fetched successfully",
"data": {
"content": [
{
"id": 5,
"name": "Laptop",
"price": 75000
},
{
"id": 2,
"name": "Phone",
"price": 60000
}
],
"page": 0,
"size": 5,
"totalElements": 120,
"totalPages": 24,
"first": true,
"last": false,
"hasNext": true,
"hasPrevious": false
}
}
Complete Flow
Client
β
GET /products?page=0&size=5&sortBy=price&direction=desc
β
Controller
β
Service
β
Create Sort
β
Create Pageable
β
Repository.findAll(pageable)
β
Spring Data JPA
β
SQL
β
Database
β
Page<Product>
β
Page<ProductResponse>
β
Controller
β
JSON Response
Best Practices
- Always use pagination for list APIs.
- Return DTOs instead of entities.
- Validate
pageandsizeto avoid invalid requests. - Restrict maximum page size (for example, 100) to prevent abuse.
- Validate
sortByagainst allowed fields to avoid invalid property errors. - Return a custom pagination DTO instead of exposing Spring’s
Pagedirectly. - Combine pagination with filtering and sorting for production-grade APIs.
Conclusion
Pagination is an essential feature for scalable REST APIs. Spring Data JPA simplifies its implementation using Page, Pageable, and PageRequest, allowing you to fetch only the required records while automatically providing useful metadata such as total pages, total elements, and navigation information.
A typical flow is:
- Receive pagination and sorting parameters in the controller.
- Create a
Sortobject based onsortByanddirection. - Build a
PageableusingPageRequest. - Pass the
Pageableto the repository. - Convert the resulting
Page<Entity>into aPage<DTO>. - Return a clean, custom pagination response to the client.
By following these practices, your APIs remain efficient, scalable, and easy for frontend applications to consume.