Implement Pagination in your Spring Boot application
Pagination in backend development is a technique used to split large sets of data into smaller, manageable chunks, making it easier to handle and display in applications. It helps improve performance by reducing the amount of data that needs to be processed and sent to the client at once, which is particularly useful in scenarios with large datasets.
Pagination is commonly implemented in RESTful APIs, where clients request specific pages of data using parameters like page number and page size. The backend server retrieves only the requested subset of records from the database, enhancing response times and reducing memory consumption.
Advantages of Pagination
- Performance Optimization: Loading a large dataset at once can be inefficient and slow, consuming significant memory and processing power. Pagination limits the amount of data processed and sent at one time, improving response times and reducing server load.
- Improved User Experience: Instead of waiting for all data to load, users receive a manageable subset quickly, leading to a smoother experience. Pagination is essential for interfaces where users typically browse through pages, like product listings or search results.
- Network Bandwidth Efficiency: By fetching data in chunks, pagination reduces the amount of data sent over the network, especially beneficial for mobile or low-bandwidth environments.
- Scalability: For databases with millions of records, pagination allows the application to scale better, as only a portion of data is loaded into memory and processed at a time.
- Resource Management: By handling data in segments, pagination reduces the risk of overloading system resources, making applications more stable under high loads.
- Easier Data Processing: When paginated, data can be processed in smaller, manageable portions. This approach simplifies operations like sorting, filtering, or additional processing without needing to handle the full dataset.
Spring Data JPA of Spring Boot provides Pageable interface to developers to use page-based pagination to fetch data from their data source. Page-based pagination uses page number and page size to fetch data based on size and page number that is given in request.
The Pageable interface in Spring Data JPA provides a way to handle pagination and sorting of database queries by allowing you to request a specific subset, or "page," of a dataset. You create a Pageable instance using PageRequest, where you specify the page number (starting at 0), page size, and optional sorting parameters. This pageable object can be passed to repository methods, like findAll(Pageable pageable) , to retrieve a Page object that contains a subset of data as well as metadata, such as total pages, total elements, and navigation information (e.g., whether there are more pages). This approach improves performance by fetching only the needed data for each page, and it makes it easy to implement pagination and sorting in applications, especially useful in REST APIs to deliver data in manageable chunks.
If we are done talking, then let’s dive in to coding!
First of all, let’s create a model that will return as response for pagination requests in our Spring Boot application:
@Data
@NoArgsConstructor
public class PagingResult<T> {
private Collection<T> content;
private Integer totalPages;
private long totalElements;
private Integer size;
private Integer page;
private boolean empty;
public PagingResult(Collection<T> content, Integer totalPages, long totalElements, Integer size, Integer page, boolean empty) {
this.content = content;
this.totalPages = totalPages;
this.totalElements = totalElements;
this.size = size;
this.page = page + 1;
this.empty = empty;
}
}
The PagingResult<T> class is a generic data structure designed to represent paginated results. It contains a collection of items (content) of type T, along with pagination metadata: totalPages (the total number of pages), totalElements (the total count of elements across all pages), size (the size of each page), page (the current page number, incremented by 1 as we want our page index to start from 1), and empty (a boolean indicating if the result set is empty).
We said that Pageable uses PageRequest to set pagination metadata in requests, we can create an util class that will have a static function with parameters to create a PageRequest. Here is how it does look like:
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PaginationUtils {
public static Pageable getPageable(Integer page, Integer size, Sort.Direction direction, String sortField) {
return PageRequest.of(page, size, direction, sortField);
}
}
PageRequest accepts as parameters:
- Page Number (number of page to retrieve data)
- Page Size (size of data that will be in a page)
- Sorting Direction (ordering data as DESCENDING or ASCENDING)
- Sorting Field (sorting data based on a field of database)
Usually in requests, pagination metadata is sent through query parameters in controller layer then it is passed to our PageRequest and returns data. But if you see, we have a lot of parameters in our static function and we will need to create same parameters for other functions of paginations.
A way to reduce parameters to one parameter is to create a PaginationRequest class that will contain these parameters as variables.
Here how does PaginationRequest look like in code:
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class PaginationRequest {
@Builder.Default
private Integer page = 1;
@Builder.Default
private Integer size = 10;
@Builder.Default
private String sortField = "id";
@Builder.Default
private Sort.Direction direction = Sort.Direction.DESC;
}
This is a request model that has our PageRequest parameters as variables that are by default initialized with values in case if there aren’t any values passed from RESTful API request.
Now let’s update our static function with this new PaginationRequest class:
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PaginationUtils {
public static Pageable getPageable(PaginationRequest request) {
return PageRequest.of(request.getPage(), request.getSize(), request.getDirection(), request.getSortField());
}
}
As you can see, it’s now more readable and maintable.
Let’s create a service that will find all users from database and return them with pagination in controller layer:
// UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository repository;
private final UserMapper mapper;
public PagingResult<UserDto> findAll(PaginationRequest request) {
final Pageable pageable = PaginationUtils.getPageable(request);
final Page<User> entities = repository.findAll(pageable);
final List<UserDto> entitiesDto = entities.stream().map(mapper::convertToDto).toList();
return new PagingResult<>(
entitiesDto,
entities.getTotalPages(),
entities.getTotalElements(),
entities.getSize(),
entities.getNumber(),
entities.isEmpty()
);
}
}
This service is returning our PagingResult generic class with UserDto as type and is accepting a PaginationRequest as parameter.
We create a Pageable instance using our util class by passing PaginationRequest parameter to static function that creates a PageRequest.
findAll() function supports Pageable by default, so we need only to initiate it as Page<User> to use pagination with findAll() function.
As we use DTO’s to return data, we take all entities and convert it to DTO with our mapper class created in project using Java Streams.
After that we return a new PagingResult with DTO’s as contents and other pagination metadata we got from Page<User> variable.
Let’s create a GET request with this service at our controller layer:
// UserController.java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService service;
@GetMapping
public ResponseEntity<GenericResponse<PagingResult<UserDto>>> findAllUsers(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) String sortField,
@RequestParam(required = false) Sort.Direction direction
) {
final PaginationRequest request = new PaginationRequest(page, size, sortField, direction);
final PagingResult<UserDto> users = service.findAll(request);
return GenericResponse.success(MessageResponse.USER_FOUND, users);
}
}
This is a GET request to endpoint: <BASE_URL>/api/v1/users that will return all users with pagination by parameters you will pass it through query parameters (in case you do not pass one, default values will be applied from PaginationRequest).
Here is a response body that will look like when you get a successful response:
{
"content": ["user1", "user2", "user3"], // Assume that content returns string
"totalPages": 5,
"totalElements": 15,
"size": 3,
"page": 1,
"empty": false
}
Pagination is a very efficient technique to manage large datasets and it is very important to be properly implemented for backend and also frontend or mobile applications. It’s very user-friendly and performance-friendly for both developers and users.
You can also check my GitHub repository for more details about pagination and Spring Boot:
Spring Boot Template by Ardi Jorganxhi
Thank you for reading!