00:00

Spring Boot Pagination and Sorting

Real applications often store thousands or even millions of rows in a table. Loading all of them in one request wastes memory, slows down the database and makes the API response huge. Pagination solves this by returning the data in small pieces, and sorting makes sure those pieces arrive in a useful order.

Spring Boot, together with Spring Data JPA, gives you both features out of the box. In this tutorial you will build a small product catalog API and learn, step by step, how to page, sort and filter data the right way.

1. What Is Pagination?

Pagination means splitting a large result into smaller, fixed-size groups called pages. The client asks for one page at a time, and the server returns only the rows that belong to that page.

Think of a book: you read one page at a time instead of looking at every word at once. If a table has 95 products and the page size is 10, the data is split like this:

  • Page 0 → products 1 to 10
  • Page 1 → products 11 to 20
  • …
  • Page 9 → products 91 to 95 (the last page holds only 5 items)

In Spring Data, page numbers start at 0, not 1.

2. What Is Sorting?

Sorting arranges records in a defined order, based on one or more fields. Each field can be sorted in ascending order (A → Z, 1 → 9, oldest → newest) or descending order (Z → A, 9 → 1, newest → oldest).

Typical examples:

  • Products by price, cheapest first
  • Orders by date, newest first
  • Customers by last name, then by first name

Sorting matters even more when you paginate. Without a fixed order, the database may return rows in a different order on each request, and users could see the same item twice or miss one completely.

3. Why Use Pagination and Sorting?

BenefitWhat It Means
Faster responsesThe database reads and sends only a few rows per request.
Lower memory useThe application never holds the whole table in memory.
Smaller payloadsLess data travels over the network, which helps mobile clients.
Better user experienceUsers see results quickly and can move page by page.
Scalable APIsResponse time stays stable as the table keeps growing.

4. Key Interfaces and Classes

Spring Data provides a small set of types in the org.springframework.data.domain package. Once you know these, the rest of the topic is easy.

TypePurpose
PageableDescribes the request: which page, how many items, and in what order.
PageRequestThe ready-made implementation of Pageable. You create it with PageRequest.of(...).
SortDescribes the order: one or more fields, each with a direction.
Sort.OrderOne sort rule for a single field, such as "price descending".
Page<T>The result: the items of the current page plus totals (total items, total pages).
Slice<T>A lighter result: the items plus a "has next page" flag, without totals.

5. Project Setup

Create a Spring Boot 3 project with the Spring Web, Spring Data JPA and H2 Database dependencies. H2 is an in-memory database, so you can run the examples without installing anything.

pom.xml
<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.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Turn on SQL logging so you can see the queries Spring generates for each page:

application.properties
spring.datasource.url=jdbc:h2:mem:catalogdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

6. Creating the Entity

Our example uses a simple Product entity. The indexes on category and price help the database sort and filter quickly, which becomes important on big tables.

Product.java
package com.javabytechie.catalog.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;

import java.math.BigDecimal;
import java.time.LocalDate;

@Entity
@Table(name = "products", indexes = {
        @Index(name = "idx_product_category", columnList = "category"),
        @Index(name = "idx_product_price", columnList = "price")
})
public class Product {

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

    private String name;
    private String category;
    private BigDecimal price;
    private LocalDate addedOn;

    protected Product() {
        // required by JPA
    }

    public Product(String name, String category, BigDecimal price, LocalDate addedOn) {
        this.name = name;
        this.category = category;
        this.price = price;
        this.addedOn = addedOn;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getCategory() { return category; }
    public BigDecimal getPrice() { return price; }
    public LocalDate getAddedOn() { return addedOn; }
}

To have something to page through, load some sample rows when the application starts:

SampleDataLoader.java
package com.javabytechie.catalog.config;

import com.javabytechie.catalog.entity.Product;
import com.javabytechie.catalog.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Component
public class SampleDataLoader implements CommandLineRunner {

    private final ProductRepository productRepository;

    public SampleDataLoader(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public void run(String... args) {
        String[] categories = {"Books", "Laptops", "Phones", "Toys"};
        List<Product> products = new ArrayList<>();

        for (int i = 1; i <= 50; i++) {
            String category = categories[i % categories.length];
            BigDecimal price = BigDecimal.valueOf(100L + (i * 37L) % 900);
            products.add(new Product("Product " + i, category, price,
                    LocalDate.of(2025, 1, 1).plusDays(i)));
        }

        productRepository.saveAll(products);
    }
}

7. Creating the Repository

JpaRepository already includes paging and sorting support, so a one-line interface is enough to get findAll(Pageable) and findAll(Sort).

You can also add your own derived query methods. If you add a Pageable or Sort parameter to them, Spring Data applies the paging and ordering for you.

ProductRepository.java
package com.javabytechie.catalog.repository;

import com.javabytechie.catalog.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

import java.math.BigDecimal;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Paged results for one category, with total counts
    Page<Product> findByCategory(String category, Pageable pageable);

    // Paged results without a count query
    Slice<Product> findByPriceLessThan(BigDecimal maxPrice, Pageable pageable);

    // Sorted results, no paging
    List<Product> findByCategory(String category, Sort sort);

    // Fixed limit and order written into the method name
    List<Product> findTop5ByOrderByPriceDesc();
}

8. Pagination Using Pageable

Build a Pageable with PageRequest.of(page, size) and pass it to the repository. Spring Data adds the correct LIMIT and OFFSET to the SQL automatically.

ProductService.java
public Page<Product> getProducts(int page, int size) {
    Pageable pageable = PageRequest.of(page, size);
    return productRepository.findAll(pageable);
}

For page = 2 and size = 10, Hibernate runs two queries: one to fetch the rows and one to count the total.

Console output
select p.id, p.added_on, p.category, p.name, p.price
from products p
offset 20 rows fetch first 10 rows only

select count(p.id)
from products p

The offset is calculated as page × size, so page 2 skips the first 20 rows. The exact SQL syntax depends on your database, but the idea is the same everywhere.

9. Understanding the Page Object

A Page holds the rows of the current page plus information about the full result. These are the methods you will use most:

MethodReturns
getContent()The list of items on the current page
getNumber()The current page number (0-based)
getSize()The requested page size
getNumberOfElements()How many items this page actually contains
getTotalElements()The total number of matching rows
getTotalPages()The total number of pages
isFirst() / isLast()Whether this is the first or last page
hasNext() / hasPrevious()Whether another page exists after or before this one
map(...)A new Page with each item converted, for example from entity to DTO
PageInfoExample.java
Page<Product> result = productRepository.findAll(PageRequest.of(0, 10));

System.out.println("Items on this page : " + result.getNumberOfElements());
System.out.println("Current page       : " + result.getNumber());
System.out.println("Total items        : " + result.getTotalElements());
System.out.println("Total pages        : " + result.getTotalPages());
System.out.println("Has next page?     : " + result.hasNext());

result.getContent().forEach(product -> System.out.println(product.getName()));

10. Sorting Using Sort

The Sort class describes the order of the results. The field names are the Java property names of the entity (for example addedOn), not the database column names.

10.1 Sort by One Field

SortExamples.java
// Cheapest first
List<Product> byPriceAsc = productRepository.findAll(Sort.by("price"));

// Most expensive first
List<Product> byPriceDesc = productRepository.findAll(Sort.by("price").descending());

// Same result, using the Direction enum
List<Product> byPriceDesc2 = productRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));

10.2 Sort by Multiple Fields

When two rows have the same value in the first field, the second field decides their order.

SortExamples.java
// Category A → Z, then price high → low inside each category
Sort sort = Sort.by("category").ascending()
        .and(Sort.by("price").descending());

List<Product> products = productRepository.findAll(sort);

10.3 Sort with Sort.Order

Sort.Order gives you finer control, such as case-insensitive sorting:

SortExamples.java
Sort sort = Sort.by(
        Sort.Order.asc("name").ignoreCase(),
        Sort.Order.desc("addedOn")
);

List<Product> products = productRepository.findAll(sort);

11. Pagination and Sorting Together

In real APIs, the client usually sends the page number, page size, sort field and sort direction. This section builds a complete, clean endpoint for that.

11.1 Response DTOs

Returning entities directly exposes your database structure. Use small DTO records instead, plus one reusable wrapper for paged responses:

ProductDto.java
package com.javabytechie.catalog.dto;

import com.javabytechie.catalog.entity.Product;

import java.math.BigDecimal;

public record ProductDto(Long id, String name, String category, BigDecimal price) {

    public static ProductDto from(Product product) {
        return new ProductDto(
                product.getId(),
                product.getName(),
                product.getCategory(),
                product.getPrice()
        );
    }
}
PagedResponse.java
package com.javabytechie.catalog.dto;

import org.springframework.data.domain.Page;

import java.util.List;

public record PagedResponse<T>(
        List<T> content,
        int page,
        int size,
        long totalElements,
        int totalPages,
        boolean first,
        boolean last
) {

    public static <T> PagedResponse<T> from(Page<T> page) {
        return new PagedResponse<>(
                page.getContent(),
                page.getNumber(),
                page.getSize(),
                page.getTotalElements(),
                page.getTotalPages(),
                page.isFirst(),
                page.isLast()
        );
    }
}

11.2 Service Layer

The service checks the input, builds the Sort and Pageable, and converts each entity to a DTO with Page.map().

ProductService.java
package com.javabytechie.catalog.service;

import com.javabytechie.catalog.dto.PagedResponse;
import com.javabytechie.catalog.dto.ProductDto;
import com.javabytechie.catalog.repository.ProductRepository;
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;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class ProductService {

    private static final int MAX_PAGE_SIZE = 50;

    private final ProductRepository productRepository;

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

    public PagedResponse<ProductDto> getProducts(int page, int size,
                                                String sortBy, String direction) {
        int safePage = Math.max(page, 0);
        int safeSize = Math.min(Math.max(size, 1), MAX_PAGE_SIZE);

        Sort.Direction dir = "desc".equalsIgnoreCase(direction)
                ? Sort.Direction.DESC
                : Sort.Direction.ASC;

        // "id" as a second sort key keeps the order stable when values repeat
        Sort sort = Sort.by(dir, sortBy).and(Sort.by("id"));

        Pageable pageable = PageRequest.of(safePage, safeSize, sort);

        Page<ProductDto> result = productRepository.findAll(pageable)
                .map(ProductDto::from);

        return PagedResponse.from(result);
    }
}

11.3 Controller

ProductController.java
package com.javabytechie.catalog.controller;

import com.javabytechie.catalog.dto.PagedResponse;
import com.javabytechie.catalog.dto.ProductDto;
import com.javabytechie.catalog.service.ProductService;
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;

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

    @GetMapping
    public PagedResponse<ProductDto> getProducts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String direction) {

        return productService.getProducts(page, size, sortBy, direction);
    }
}

11.4 Testing the Endpoint

Start the application and call the API. This request asks for the first page of 3 products, most expensive first:

Terminal
curl "http://localhost:8080/api/products?page=0&size=3&sortBy=price&direction=desc"

The response contains the products along with all the paging details the client needs to build "Previous" and "Next" buttons. With the sample data from section 6, it looks like this:

Response
{
  "content": [
    { "id": 24, "name": "Product 24", "category": "Books", "price": 988 },
    { "id": 48, "name": "Product 48", "category": "Books", "price": 976 },
    { "id": 23, "name": "Product 23", "category": "Toys", "price": 951 }
  ],
  "page": 0,
  "size": 3,
  "totalElements": 50,
  "totalPages": 17,
  "first": true,
  "last": false
}

12. Pageable as a Controller Parameter

Spring MVC can also build the Pageable for you. Just declare it as a method parameter, and Spring reads the page, size and sort values from the URL.

ProductController.java
@GetMapping("/by-category")
public PagedResponse<ProductDto> getByCategory(
        @RequestParam String category,
        @PageableDefault(size = 10, sort = "price", direction = Sort.Direction.ASC)
        Pageable pageable) {

    Page<ProductDto> result = productRepository
            .findByCategory(category, pageable)
            .map(ProductDto::from);

    return PagedResponse.from(result);
}

@PageableDefault sets the values used when the client sends nothing. Here are a few example URLs:

Example requests
# Defaults: page 0, 10 items, sorted by price ascending
GET /api/products/by-category?category=Books

# Second page with 5 items
GET /api/products/by-category?category=Books&page=1&size=5

# Sort by price descending, then by name ascending
GET /api/products/by-category?category=Books&sort=price,desc&sort=name,asc

The sort parameter uses the format field,direction. Repeat it to sort by more than one field. If you leave out the direction, ascending is used.

13. Global Pagination Settings

Spring Boot lets you change the default paging behavior for the whole application in application.properties. These settings apply whenever Pageable is resolved from a request.

application.properties
# Page size used when the client does not send one (default: 20)
spring.data.web.pageable.default-page-size=10

# Largest page size a client may request (default: 2000)
spring.data.web.pageable.max-page-size=100

# Let clients start counting pages at 1 instead of 0 (default: false)
spring.data.web.pageable.one-indexed-parameters=true

# Rename the request parameters if needed
spring.data.web.pageable.page-parameter=page
spring.data.web.pageable.size-parameter=size
spring.data.web.sort.sort-parameter=sort

If a client asks for more than max-page-size, Spring silently reduces the size to that limit. This simple setting protects your API from requests like size=1000000.

14. Page vs Slice vs List

A repository method can return one of three types when it receives a Pageable. Choose based on what your screen or client really needs.

Return TypeCount Query?Knows Total Pages?Best For
Page<T>YesYesTables with page numbers such as "Page 3 of 17"
Slice<T>NoNo, only hasNext()"Load more" buttons and infinite scrolling
List<T>NoNoWhen you only need the rows themselves

A Slice works by fetching one extra row (size + 1). If that extra row exists, there is a next page. This avoids the COUNT query, which can be slow on very large tables.

SliceExample.java
Slice<Product> slice = productRepository.findByPriceLessThan(
        BigDecimal.valueOf(500), PageRequest.of(0, 10, Sort.by("price")));

slice.getContent().forEach(p -> System.out.println(p.getName()));

if (slice.hasNext()) {
    Slice<Product> next = productRepository.findByPriceLessThan(
            BigDecimal.valueOf(500), slice.nextPageable());
}

15. Pagination with Custom Queries

Paging also works with @Query. Spring Data creates the count query for simple JPQL by itself, but for complex queries it is safer to write the count query yourself with countQuery.

ProductRepository.java
// JPQL: search products by part of their name
@Query(value = "SELECT p FROM Product p "
             + "WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))",
       countQuery = "SELECT COUNT(p) FROM Product p "
             + "WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :keyword, '%'))")
Page<Product> searchByName(@Param("keyword") String keyword, Pageable pageable);

// Native SQL: the count query is required for paging
@Query(value = "SELECT * FROM products WHERE category = :category",
       countQuery = "SELECT COUNT(*) FROM products WHERE category = :category",
       nativeQuery = true)
Page<Product> findByCategoryNative(@Param("category") String category, Pageable pageable);

Do not write ORDER BY inside a paged query. Let the Sort inside the Pageable control the order, otherwise the two can conflict.

16. Validating Sort Fields

If a client sends a field that does not exist, such as sortBy=colour, Spring Data throws a PropertyReferenceException and the client sees a 500 error. Accepting any field name also lets clients sort by columns you never meant to expose.

The fix is simple: keep a list of allowed fields and reject everything else with a clear 400 Bad Request.

ProductService.java
private static final Set<String> ALLOWED_SORT_FIELDS =
        Set.of("id", "name", "category", "price", "addedOn");

private void validateSortField(String sortBy) {
    if (!ALLOWED_SORT_FIELDS.contains(sortBy)) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                "Cannot sort by '" + sortBy + "'. Allowed fields: " + ALLOWED_SORT_FIELDS);
    }
}

Call validateSortField(sortBy) at the start of getProducts(...), before building the Sort.

17. Large Tables and Keyset Scrolling

Offset pagination has one weakness: to show page 10,000, the database must still read and skip all the rows before it. On very large tables, the deep pages become slow.

Keyset pagination (also called seek or cursor pagination) avoids this. Instead of "skip N rows", it asks for "rows after the last one I saw", which an index can answer quickly. Spring Data 3.1 and later support this through ScrollPosition and Window:

ProductRepository.java
// Returns up to 10 products after the given position, ordered by id
Window<Product> findFirst10ByOrderByIdAsc(ScrollPosition position);
KeysetExample.java
Window<Product> window = productRepository.findFirst10ByOrderByIdAsc(ScrollPosition.keyset());

while (!window.isEmpty()) {
    window.forEach(product -> System.out.println(product.getName()));

    if (!window.hasNext()) {
        break;
    }
    // Continue right after the last product of the current window
    window = productRepository.findFirst10ByOrderByIdAsc(
            window.positionAt(window.size() - 1));
}

Keyset scrolling is ideal for infinite feeds and batch jobs. The trade-off is that users cannot jump straight to "page 500", so normal offset paging is still the better choice for small and medium tables.

18. Best Practices

  • Paginate every list endpoint. A table that is small today may hold millions of rows next year.
  • Set a maximum page size with max-page-size or by clamping the value in your service.
  • Always sort paged data, and add a unique field such as id as the last sort key so the order never changes between requests.
  • Allow only known sort fields to avoid errors and hide internal columns.
  • Return DTOs, not entities, and wrap them in a clear response object with paging metadata.
  • Add indexes on columns that are often used for sorting and filtering.
  • Use Slice when totals are not needed to skip the extra count query.
  • Consider keyset scrolling for very large tables or infinite scrolling feeds.

19. Common Mistakes to Avoid

  • Assuming pages start at 1. Spring Data starts at 0 unless you enable one-indexed-parameters.
  • Paging without sorting. The database may return rows in a random order, so items repeat or go missing across pages.
  • Loading everything and paging in Java. Calling findAll() and then using subList() defeats the whole purpose.
  • Using database column names in Sort. JPQL sorting expects entity property names such as addedOn, not added_on.
  • Forgetting countQuery in native queries. Without it, paging a native query can fail or return wrong totals.
  • Fetching collections with JOIN FETCH while paging. Hibernate then pages in memory and logs a warning. Fetch the IDs first, or use an entity graph on a single-valued relation.
  • Sending the whole Page object as JSON. Its structure is internal and may change between versions. Use your own DTO instead.

20. Conclusion

Pagination and sorting are must-have features for any API that returns lists. With Spring Data JPA, you only need a Pageable to page your data, a Sort to order it, and a Page or Slice to read the result.

For production APIs, add a few safety steps: limit the page size, allow only known sort fields, keep the order stable with a unique tie-breaker, and return clean DTOs. For huge tables, switch to keyset scrolling with Window and ScrollPosition. With these habits, your APIs stay fast as your data grows.

21. Frequently Asked Questions (FAQ)

Q1. What is the difference between pagination and sorting?

Pagination decides how many records to return and which part of the result to return. Sorting decides the order of those records.

They are usually used together: first the data is sorted, then the requested page is cut out of the sorted result.

Q2. Do page numbers in Spring Data start at 0 or 1?

They start at 0. PageRequest.of(0, 10) returns the first page.

If your clients prefer to start at 1, add this property. Spring then treats ?page=1 as the first page:

spring.data.web.pageable.one-indexed-parameters=true
Q3. What is the difference between Pageable and PageRequest?

Pageable is an interface that describes a page request. PageRequest is the standard class that implements it.

Write your method parameters as Pageable, and create actual objects with PageRequest.of(page, size) or PageRequest.of(page, size, sort).

Q4. What happens if I ask for a page that does not exist?

If the page number is too high, Spring returns an empty page. The content is empty, but getTotalElements() and getTotalPages() still show the correct totals.

If the page number is negative or the size is less than 1, PageRequest.of(...) throws an IllegalArgumentException. Check the values before creating the request.

Q5. What is the default page size in Spring Boot?

When Pageable is resolved from a web request and the client sends no size, the default is 20. The default maximum size is 2000.

You can change both with spring.data.web.pageable.default-page-size and spring.data.web.pageable.max-page-size.

Q6. How do I stop clients from requesting very large pages?

There are two simple options:

  1. Set spring.data.web.pageable.max-page-size=100 when you take Pageable directly as a controller parameter.
  2. Limit the value yourself when you read size as a plain number:
int safeSize = Math.min(Math.max(size, 1), 100);
Q7. How do I sort by more than one field?

Combine sort rules with and(), or list several Sort.Order objects:

Sort sort = Sort.by("category").ascending()
        .and(Sort.by("price").descending());

// or
Sort sameSort = Sort.by(Sort.Order.asc("category"), Sort.Order.desc("price"));

The first field has the highest priority. The second field only matters when two rows have the same value in the first field.

Q8. How do I pass sorting from the URL?

When your controller takes a Pageable parameter, use the sort parameter in the format field,direction. Repeat it for each extra field:

GET /api/products?page=0&size=10&sort=price,desc&sort=name,asc

If you leave out the direction (for example sort=name), ascending order is used.

Q9. What is the difference between Page and Slice?

A Page knows the total number of items and pages, because Spring runs an extra COUNT query.

A Slice only knows whether a next page exists. It skips the count query, so it is faster on large tables. Use it for "Load more" buttons and infinite scrolling.

Q10. Why does a paged query run two SQL statements?

When a method returns Page, Spring Data runs:

  1. A select query with LIMIT/OFFSET to get the rows of the page.
  2. A count query to find the total number of matching rows.

If you do not need the totals, return a Slice or List to avoid the second query. Spring also skips the count automatically when the first page already holds all results.

Q11. How do I write a count query for a custom @Query?

Add the countQuery attribute. It should use the same WHERE conditions as the main query:

@Query(value = "SELECT p FROM Product p WHERE p.price > :minPrice",
       countQuery = "SELECT COUNT(p) FROM Product p WHERE p.price > :minPrice")
Page<Product> findExpensive(@Param("minPrice") BigDecimal minPrice, Pageable pageable);
Q12. Can I use pagination with native SQL queries?

Yes. Set nativeQuery = true, add a Pageable parameter, and provide a countQuery:

@Query(value = "SELECT * FROM products WHERE category = :category",
       countQuery = "SELECT COUNT(*) FROM products WHERE category = :category",
       nativeQuery = true)
Page<Product> findByCategoryNative(@Param("category") String category, Pageable pageable);

With native queries, the sort field names in the Pageable must match database column names, because the SQL is passed to the database as written.

Q13. How can I sort text without caring about upper and lower case?

Call ignoreCase() on a Sort.Order:

Sort sort = Sort.by(Sort.Order.asc("name").ignoreCase());

Now "apple", "Banana" and "cherry" are sorted alphabetically, instead of placing all upper-case values first.

Q14. How do I sort by a field of a related entity?

Use a dot path to the property. For example, if Product had a supplier relation with a name field:

Sort sort = Sort.by("supplier.name").ascending();

From a URL it looks like ?sort=supplier.name,asc. Spring Data adds the needed join for you.

Q15. What error do I get if the sort field does not exist?

Spring Data throws a PropertyReferenceException, for example "No property 'colour' found for type 'Product'". If nothing handles it, the client receives a 500 error.

Check the field against a list of allowed names first, and return 400 Bad Request for anything else (see section 16).

Q16. Should I return Page directly from a REST controller?

It is better not to. The JSON shape of PageImpl is an internal detail and can change between versions. Recent Spring Data versions log a warning when you serialize it directly.

You have two clean options:

  • Return your own DTO, such as the PagedResponse record in this tutorial.
  • Enable Spring Data's stable format:
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
Q17. How do I convert a Page of entities into a Page of DTOs?

Use the map() method. It converts every item and keeps all the paging information:

Page<ProductDto> dtoPage = productRepository.findAll(pageable)
        .map(ProductDto::from);
Q18. Why are deep pages slow, and how can I fix it?

With offset paging, the database must read and skip every row before the requested page. Page 10,000 with size 20 means skipping 200,000 rows first.

To fix it, use keyset scrolling with Window and ScrollPosition.keyset() (Spring Data 3.1+). It continues from the last row you saw, so it stays fast no matter how far you go. Indexes on the sort columns help both approaches.

Q19. How do I get all records without pagination?

Pass Pageable.unpaged() to a method that expects a Pageable, or call findAll(Sort) if you only need ordering:

Page<Product> all = productRepository.findByCategory("Books", Pageable.unpaged());

List<Product> sorted = productRepository.findAll(Sort.by("name"));

Use this carefully. Loading everything from a large table can use a lot of memory.

Q20. How do I test pagination in a repository?

Use @DataJpaTest. It starts only the JPA layer with an in-memory database, so tests run quickly:

@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void returnsSecondPageSortedByPrice() {
        for (int i = 1; i <= 25; i++) {
            productRepository.save(new Product("Item " + i, "Books",
                    BigDecimal.valueOf(i), LocalDate.now()));
        }

        Page<Product> page = productRepository.findAll(
                PageRequest.of(1, 10, Sort.by("price")));

        assertEquals(10, page.getNumberOfElements());
        assertEquals(25, page.getTotalElements());
        assertEquals(3, page.getTotalPages());
        assertEquals(BigDecimal.valueOf(11), page.getContent().get(0).getPrice());
    }
}