Spring Boot Pagination and Sorting with Spring Data JPA
Introduction
Imagine your application has 10 employees.
Returning all 10 records from a database isn't a problem.
Now imagine your application has 100,000 employees.
Returning every employee in a single REST API response would be inefficient and unnecessary.
This is where pagination becomes important.
Pagination divides a large result set into smaller pages so that an API can return only the records the client currently needs.
Sorting allows the client to control the order of those records.
For example:
GET /api/employees?page=0&size=10
can request the first 10 employees.
You could then sort them:
GET /api/employees?page=0&size=10&sort=name,asc
Spring Data provides Pageable, Page, PageRequest, and Sort abstractions specifically for these kinds of operations.
In this tutorial, you'll learn how to implement pagination and sorting in a Spring Boot REST API using Spring Data JPA.
What Is Pagination?
Pagination means dividing a large collection of records into smaller sections called pages.
For example, suppose your database contains 50 employees and you request:
page = 0 size = 10
The API returns the first 10 records.
Then:
page = 1 size = 10
returns the next 10 records.
The concept looks like this:
50 Employees │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ Page 0 Page 1 Page 2 1 - 10 11 - 20 21 - 30 ...
This prevents the application from unnecessarily returning the entire dataset.
Why Is Pagination Important?
Without pagination:
Database ↓ 100,000 records ↓ Application ↓ Huge JSON response ↓ Slow API
With pagination:
Database ↓ Requested page ↓ 10 records ↓ Small JSON response ↓ Faster API
Pagination is particularly useful for:
- Employee management systems
- E-commerce applications
- Product catalogs
- Customer databases
- Blog applications
- Order management systems
- Search results
- Admin dashboards
What Is Sorting?
Sorting determines the order in which records are returned.
For example, employees could be sorted by name:
Adam Brian David John Michael
or by salary:
45000 55000 70000 85000 100000
Sorting can be:
Ascending
or:
Descending
For example:
name,asc
means alphabetical ascending order.
While:
salary,desc
means highest salary first.
Spring Data JPA supports dynamic sorting through Sort and Pageable.
Pagination and Sorting Architecture
Here's the flow we'll build:
Client │ │ page=0&size=10&sort=name,asc ↓ Spring Boot Controller │ ↓ Pageable │ ↓ Service Layer │ ↓ Repository │ ↓ Spring Data JPA │ ↓ Database │ ↓ Page<Employee> │ ↓ JSON Response
Create a Spring Boot Project
For this example, we'll use:
- Spring Boot
- Spring Web
- Spring Data JPA
- MySQL
- Java
If you're following our earlier CRUD tutorial, you can use the same project.
Maven Dependencies
Make sure your pom.xml contains the required dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>
Spring Boot's Data JPA starter provides the infrastructure needed to work with Spring Data JPA repositories.
Create the Employee Entity
Let's create a simple Employee entity.
@Entity @Table(name = "employees") public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String department; private double salary; // Getters and setters }
Our database might contain:
| ID | Name | Department | Salary |
|---|---|---|---|
| 1 | John | IT | 70000 |
| 2 | Sarah | HR | 60000 |
| 3 | David | IT | 80000 |
| 4 | Emma | Finance | 75000 |
| 5 | Michael | Sales | 65000 |
Imagine that this table eventually contains thousands of records.
That's where pagination becomes useful.
Create the Repository
Create:
EmployeeRepository.java
public interface EmployeeRepository extends JpaRepository<Employee, Long> { }
That's all we need.
We don't have to manually write SQL for basic pagination.
Spring Data provides pagination and sorting support through repository abstractions.
Using Pageable
The most important class in this tutorial is:
Pageable
It represents pagination information such as:
- Page number
- Page size
- Sorting
For example:
Pageable pageable = PageRequest.of(0, 10);
This means:
Page: 0 Size: 10
What Is PageRequest?
PageRequest is an implementation of Pageable.
For example:
PageRequest.of(0, 10);
means:
Page number = 0 Page size = 10
Spring Data's documentation uses PageRequest to construct pageable queries.
Important: Page Numbers Start at Zero
This is one of the most common things beginners get wrong.
Spring Data pagination is zero-based.
That means:
page=0 → first page page=1 → second page page=2 → third page
For example:
Page 0 → Records 1-10 Page 1 → Records 11-20 Page 2 → Records 21-30
Create a Paginated Repository Method
Because JpaRepository supports Pageable, we can write:
Page<Employee> findAll(Pageable pageable);
However, JpaRepository already provides a suitable findAll(Pageable) method, so you generally don't need to redeclare it.
That means our repository can remain:
public interface EmployeeRepository extends JpaRepository<Employee, Long> { }
Create the Service Layer
Create:
EmployeeService.java
@Service public class EmployeeService { private final EmployeeRepository employeeRepository; public EmployeeService( EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; } public Page<Employee> getEmployees( Pageable pageable) { return employeeRepository .findAll(pageable); } }
The service receives the Pageable object and passes it to the repository.
Create the REST Controller
Now create:
EmployeeController.java
@RestController @RequestMapping("/api/employees") public class EmployeeController { private final EmployeeService employeeService; public EmployeeController( EmployeeService employeeService) { this.employeeService = employeeService; } @GetMapping public Page<Employee> getEmployees( Pageable pageable) { return employeeService .getEmployees(pageable); } }
Spring can resolve pagination information from request parameters into a Pageable parameter.
Test Pagination
Start your application and request:
http://localhost:8080/api/employees?page=0&size=10
This requests:
Page = 0 Size = 10
The response contains page information along with the content.
A response can look conceptually like:
{ "content": [ { "id": 1, "name": "John", "department": "IT" } ], "number": 0, "size": 10, "totalElements": 50, "totalPages": 5 }
The exact JSON structure depends on your Spring Data/Spring Boot configuration.
Understanding Page<T>
When your repository returns:
Page<Employee>
you get more than just employee records.
A Page contains information such as:
Content Current page Page size Total elements Total pages First/last indicators
This metadata is extremely useful for frontend applications.
For example:
Total records: 100 Page size: 10 Total pages: 10 Current page: 3
The Spring Data documentation explains that a Page provides total-element and total-page information, which requires a count query.
Add Sorting
Now let's add sorting.
Use:
sort=name,asc
For example:
http://localhost:8080/api/employees?page=0&size=10&sort=name,asc
This means:
Page: 0 Size: 10 Sort: name Direction: ascending
Sort in Descending Order
To sort from highest to lowest or Z to A:
sort=name,desc
Example:
http://localhost:8080/api/employees?page=0&size=10&sort=name,desc
Spring Data supports sorting through the Pageable abstraction.
Sort by Salary
You can sort by another entity property:
sort=salary,desc
Example:
GET /api/employees?page=0&size=10&sort=salary,desc
The result will contain the highest-paid employees first.
Multiple Sorting Fields
You can also sort using multiple properties.
For example:
sort=department,asc&sort=name,asc
This means:
- Sort by department
- Then sort by employee name
Spring Data supports multiple sort expressions through Sort/Pageable.
Using Sort Directly
If you don't need pagination and only need sorting, you can use Sort.
Example:
Sort sort = Sort.by("name").ascending(); List<Employee> employees = employeeRepository.findAll(sort);
You can also create descending sorting:
Sort sort = Sort.by("salary").descending();
Spring Data JPA supports both Sort and Pageable as dynamic query parameters.
Pagination + Sorting Together
This is where PageRequest becomes particularly useful.
Pageable pageable = PageRequest.of( 0, 10, Sort.by("name").ascending() );
Now we have:
Page → 0 Size → 10 Sort → name ASC
Then:
Page<Employee> employees = employeeRepository .findAll(pageable);
Using PageRequest in a Service
You can also create the Pageable object yourself.
public Page<Employee> getEmployees( int page, int size) { Pageable pageable = PageRequest.of(page, size); return employeeRepository .findAll(pageable); }
Then add sorting:
public Page<Employee> getEmployees( int page, int size) { Pageable pageable = PageRequest.of( page, size, Sort.by("name").ascending() ); return employeeRepository .findAll(pageable); }
A Better REST API Design
For a real application, you might expose:
GET /api/employees?page=0&size=10&sort=name,asc
This gives the frontend control over:
- Which page to retrieve
- How many records to retrieve
- How records should be sorted
The complete request flow is:
GET /api/employees │ ├── page=0 ├── size=10 └── sort=name,asc │ ↓ Pageable │ ↓ EmployeeRepository │ ↓ Database │ ↓ Page<Employee>
Pagination with a Custom Query
Pagination isn't limited to findAll().
Suppose we want employees from a particular department.
Page<Employee> findByDepartment( String department, Pageable pageable);
Then:
Page<Employee> employees = employeeRepository .findByDepartment( "IT", pageable );
Spring Data JPA supports Pageable parameters on query methods.
Create a Search + Pagination API
We can combine searching, pagination, and sorting.
Repository:
Page<Employee> findByNameContainingIgnoreCase( String name, Pageable pageable);
Controller:
@GetMapping("/search") public Page<Employee> searchEmployees( @RequestParam String name, Pageable pageable) { return employeeRepository .findByNameContainingIgnoreCase( name, pageable); }
Now the API can be called like:
/api/employees/search?name=john&page=0&size=10&sort=name,asc
This is much closer to what you'll find in a real-world application.
Page vs Slice
Spring Data provides both Page and Slice.
Page
A Page provides total information such as:
Total elements Total pages Current page
But determining that information can require an additional count query.
Slice
A Slice focuses on whether another portion of results is available.
For example:
Slice<Employee> findByDepartment( String department, Pageable pageable);
If your UI only needs:
Previous Next
and doesn't need:
Page 1 of 250
a Slice may be more appropriate.
Spring Data specifically notes that a Slice avoids the total-count requirement that a Page uses.
Page vs Slice — Which Should You Use?
| Requirement | Recommended |
|---|---|
| Need total pages | Page |
| Need total records | Page |
| Need only next/previous | Slice |
| Large result traversal | Consider Slice |
| Traditional numbered pagination | Page |
Don't automatically use Page for every query.
Choose based on what your application actually needs.
Protect the Page Size
One practical issue is allowing clients to request an extremely large page.
For example:
?page=0&size=1000000
You generally don't want an API consumer to request an enormous number of records.
Set a reasonable maximum page size at the application/API layer.
For example, your API might allow:
size <= 100
This is especially important for public APIs.
Validate Pagination Parameters
A robust API should also consider invalid values.
For example:
page=-1 size=0 size=100000
Don't assume every client sends valid pagination parameters.
You can define sensible defaults and enforce maximum values.
Pagination and Performance
Pagination improves API behavior, but it doesn't magically make every database query fast.
For example, a query with:
OFFSET 900000
can still become expensive on very large datasets.
For extremely large result sets, other approaches such as scrolling or keyset-style techniques may be more appropriate.
Current Spring Data JPA documentation discusses offset-based and keyset-based scrolling as alternatives for large datasets.
Offset Pagination vs Keyset Pagination
Traditional pagination commonly works around:
page size offset
Conceptually:
Database ↓ Skip records ↓ Return requested records
Keyset pagination works differently.
Instead of saying:
Give me page 500
you can say:
Give me records after this last ID
Conceptually:
Last record from previous request ↓ Cursor ↓ Next records
For extremely large datasets, keyset approaches can avoid some of the performance problems associated with deep offset pagination. Spring Data JPA's current documentation discusses keyset-based scrolling and its use of database indexes.
Common Pagination Mistakes
Mistake 1: Forgetting That Pages Start at Zero
Remember:
0 = first page 1 = second page 2 = third page
Mistake 2: Returning a List for Huge Data
Instead of:
List<Employee>
consider:
Page<Employee>
when the client needs pagination metadata.
Mistake 3: Allowing Unlimited Page Size
Don't allow:
size=1000000
without a good reason.
Mistake 4: Ignoring Database Performance
Pagination reduces the amount returned to the client, but database queries still need to be designed appropriately.
Mistake 5: Sorting by Uncontrolled Properties
If clients can provide arbitrary sort fields, validate the fields your API actually supports.
For example, explicitly allow:
name salary department
instead of accepting every possible property.
Best Practices for Spring Boot Pagination
Use Pageable
Let Spring Data handle pagination information rather than manually calculating offsets.
Use Page When Metadata Matters
If the UI needs total pages or total records, Page is useful.
Use Slice When Total Counts Aren't Needed
This can avoid an additional count query.
Set Maximum Page Size
Prevent clients from requesting huge result sets.
Validate Sort Fields
Don't blindly trust arbitrary sort properties from public clients.
Use Indexes
Frequently searched or sorted database columns may benefit from appropriate database indexes.
Consider Keyset Pagination for Very Large Data
Traditional page-number pagination isn't always ideal for extremely large datasets.
Frequently Asked Questions
What is pagination in Spring Boot?
Pagination allows a Spring Boot application to retrieve a limited portion of a larger dataset instead of returning all records at once.
What is Pageable in Spring Data JPA?
Pageable represents pagination and sorting information that can be passed to repository query methods.
What is PageRequest?
PageRequest is a Pageable implementation used to define page number, page size, and optionally sorting.
Does Spring Data JPA support sorting?
Yes. Spring Data JPA supports dynamic sorting through Sort and Pageable.
Are Spring Data page numbers zero-based?
Yes. The first page is page 0.
What is the difference between Page and Slice?
Page provides total-count and total-page information, while Slice focuses on whether another slice is available and does not require the total count.
Can pagination and sorting be used together?
Yes. PageRequest can contain both pagination and sorting information.
Example:
PageRequest.of( 0, 10, Sort.by("name").ascending() );
Conclusion
Pagination and sorting are essential when building REST APIs that work with large amounts of data.
With Spring Data JPA, you don't need to manually write complicated pagination logic for common use cases.
The key classes and interfaces to remember are:
Pageable PageRequest Page Slice Sort
A typical request can look like:
GET /api/employees?page=0&size=10&sort=name,asc
The request flows through the application like this:
Client ↓ Controller ↓ Pageable ↓ Service ↓ Repository ↓ Database ↓ Page<Employee> ↓ JSON Response
Once you understand this pattern, you can apply it to employee systems, e-commerce products, customer records, orders, blog posts, and almost any application that needs to retrieve large collections efficiently.
