Skip to main content

Pagination and Sorting in Spring Data JPA

In enterprise systems handling thousands or millions of records, executing unconstrained queries such as findAll() exhausts network bandwidth, consumes excessive JVM memory (heap), and degrades user experience. Pagination and sorting split large datasets into manageable discrete chunks (pages) sorted efficiently directly by the SQL database engine.


1. Concepts and Theoretical Foundations

Comparison between unpaginated query and segmented query with Pageable

Core Pagination Architecture Components

  1. Pageable: A Spring Data interface encapsulating pagination and sorting parameters sent by the client:
    • Page number (page, zero-based index).
    • Page size (size, number of items per page).
    • Sorting configuration (Sort).
  2. PageRequest: The primary concrete implementation of Pageable, instantiated via factory methods like PageRequest.of(page, size, sort).
  3. Sort: Defines the sorting criteria over one or more entity attributes, specifying direction (Sort.Direction.ASC or Sort.Direction.DESC).
  4. Page<T>: A Spring Data sub-interface that contains not only the requested page items (getContent()), but also computed pagination metadata (getTotalElements(), getTotalPages(), getNumber(), hasNext(), hasPrevious()).
  5. Slice<T>: An alternative to Page<T> that knows whether a next slice exists, but does not execute an additional COUNT(*) query. It is ideal for infinite scroll interfaces in mobile applications.

Benefits of Implementing Pagination

BenefitTechnical Impact
Prevents Out-of-Memory (OOM)Prevents the JVM from loading hundreds of thousands of entities into heap memory, avoiding java.lang.OutOfMemoryError.
Low Network Overhead & Lightweight PayloadsTransfers compact JSON payloads (20–50 items) in milliseconds rather than multi-megabyte responses.
Native SQL TranslationSpring Data automatically translates Pageable into native database clauses (LIMIT and OFFSET in PostgreSQL/MySQL or FETCH FIRST in Oracle).
Frontend Table CompatibilityDirectly matches the data requirements of UI pagination components (such as React paginated tables or DataTables).

Execution Flow in Spring Data

Clic para ampliar

The diagram above illustrates the two-step querying executed by Page<T>: first, it fetches the paginated window using LIMIT and OFFSET, and second, it runs a count query to obtain the total number of matching records.


2. Practical Guide: Step-by-Step

Follow these steps to configure a JPA entity, a paginated repository, a service layer, and an exposed REST endpoint.

1

Define the JPA Entity

Create the base entity using standard JPA and Lombok annotations to manage product catalog data.

src/main/java/com/icesi/store/model/Product.java
package com.icesi.store.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "products")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// Commercial product name
private String name;

// Category for filtering
private String category;

// Unit price used for sorting
private Double price;

// Available inventory units
private Integer stock;
}
2

Create the Repository with PagingAndSortingRepository

In Spring Data JPA, JpaRepository already extends PagingAndSortingRepository. Therefore, any standard repository supports paginated methods out of the box.

src/main/java/com/icesi/store/repository/ProductRepository.java
package com.icesi.store.repository;

import com.icesi.store.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

// Derived method: filters by category and applies pagination
Page<Product> findByCategory(String category, Pageable pageable);

// Case-insensitive partial name search with pagination
Page<Product> findByNameContainingIgnoreCase(String keyword, Pageable pageable);
}
How does findAll work?

The JpaRepository interface already declares Page<T> findAll(Pageable pageable). You do not need to redeclare it in your interface unless you are applying custom query filters.

3

Implement Business Logic in the Service Layer

The service layer receives request parameters, validates sorting direction, and constructs the Pageable instance using PageRequest.

src/main/java/com/icesi/store/service/ProductService.java
package com.icesi.store.service;

import com.icesi.store.model.Product;
import com.icesi.store.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

private final ProductRepository productRepository;

@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

/**
* Retrieves paginated products with dynamic sorting.
*
* @param page Page index (0-based)
* @param size Number of items per page
* @param sortBy Entity attribute to sort by
* @param direction Direction ("asc" or "desc")
* @return Page of products with metadata
*/
public Page<Product> getProducts(int page, int size, String sortBy, String direction) {
// Resolve sort direction safely
Sort.Direction sortDirection = direction.equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;

// Build Sort specification
Sort sort = Sort.by(sortDirection, sortBy);

// Instantiate Pageable with page index, page size, and sort rules
Pageable pageable = PageRequest.of(page, size, sort);

// Execute query through repository
return productRepository.findAll(pageable);
}

/**
* Paginated category filter with default price ascending sort.
*/
public Page<Product> getProductsByCategory(String category, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("price").ascending());
return productRepository.findByCategory(category, pageable);
}
}
4

Expose the REST Endpoint in the Controller

Use @RequestParam with fallback defaults (defaultValue) so requests work seamlessly even without query string parameters.

src/main/java/com/icesi/store/controller/ProductController.java
package com.icesi.store.controller;

import com.icesi.store.model.Product;
import com.icesi.store.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping
public ResponseEntity<Page<Product>> getAllProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {

Page<Product> productPage = productService.getProducts(page, size, sortBy, direction);
return ResponseEntity.ok(productPage);
}
}
5

Inspect the JSON Response Structure

When a client sends GET /api/products?page=0&size=2&sortBy=price&direction=desc, Page<T> produces the following JSON structure:

HTTP 200 OK Response
{
"content": [
{
"id": 84,
"name": "Pro Gaming Laptop",
"category": "Computers",
"price": 4500000.0,
"stock": 12
},
{
"id": 12,
"name": "34-inch Curved Monitor",
"category": "Displays",
"price": 2800000.0,
"stock": 5
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 2,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"totalElements": 150,
"totalPages": 75,
"last": false,
"first": true,
"size": 2,
"number": 0,
"numberOfElements": 2,
"empty": false
}
Direct Pageable Injection in Controller Parameters

Spring MVC can bind Pageable directly as a controller parameter:

@GetMapping
public ResponseEntity<Page<Product>> listProducts(Pageable pageable) {
return ResponseEntity.ok(productRepository.findAll(pageable));
}

Spring automatically parses request parameters such as ?page=0&size=10&sort=price,desc without manual binding.


3. Self-Assessment Quiz

Cargando cuestionario...