Interviewers love pagination because it quietly tests REST design, database thinking, and API ergonomics all at once.
Question: What is pagination in Spring Boot, and how do you use it in a REST API?
Answer: Pagination means returning a large result set in smaller chunks instead of sending everything at once. In Spring Boot, you usually do this with Spring Data’s Pageable and Page<T>, which let the client ask for a page number, a page size, and optional sorting. This keeps responses faster, reduces memory use, and makes APIs easier to consume.
Interview-Ready Answer: “In Spring Boot, I paginate REST APIs by accepting a Pageable in the controller and returning a Page<T> or a DTO built from it. The client sends page, size, and optionally sort; Spring Data converts that into database pagination, usually with limit and offset. I like this approach because it keeps payloads small and also gives metadata like total pages. For very large datasets, I may switch to Slice or keyset pagination to avoid expensive count queries.”
Pagination is just controlled slicing of a result set. Instead of returning 50,000 rows, the API returns 20, 50, or 100 rows at a time. In Spring Boot, the common building blocks are:
Pageable — the request object that says which slice you want.Page<T> — the response object with content plus totals.Slice<T> — like a page, but without the total count.By default, Spring Data uses zero-based page numbers, so the first page is page=0. The default page size is typically 20. You can change these globally with properties like spring.data.web.pageable.default-page-size and spring.data.web.pageable.one-indexed-parameters=true, or per endpoint with @PageableDefault.
/api/products?page=0&size=10&sort=name,asc.Pageable and uses a resolver called PageableHandlerMethodArgumentResolver to build the request object.Pageable and translates it into database pagination, usually LIMIT plus OFFSET with an ORDER BY clause.Page<T>, Spring Data usually runs a second query to count the total rows. That is what lets it answer questions like “how many pages exist?”Memory idea: think of pagination like a library shelf. sort decides the order of the books, page chooses the shelf number, and size tells you how many books to carry out.
It also improves user experience: a page of 20 items loads quickly, and the client can request the next chunk only if needed.
| Type | Gives you | Count query | Best for |
|---|---|---|---|
Page<T> | Items + totals | Yes | Classic paged UI |
Slice<T> | Items + next/prev | No | Infinite scroll |
| Cursor | Next token | No | Huge feeds |
Page is the most interview-friendly choice because it gives totalElements and totalPages. Slice is lighter because it avoids the count query; Spring only needs one extra row to know whether another slice exists. Cursor/keyset pagination is best for very deep or rapidly changing datasets, because offset-based paging can become slow and unstable.
offset + size. A request like page 10,000 with size 20 may force the database to skip about 200,000 rows.Page<T> often means two queries: one for data and one for count. On large joins, the count can become the slow part.O(size) instead of O(total rows).id or a timestamp plus id. Without that, page 2 may repeat or skip rows when new records arrive.Version note: the core behavior is the same in Spring Boot 2 and 3. The big Boot 3 change is the move from javax to jakarta packages for JPA and validation, not pagination itself.
400 Bad Request.So the short mental model is: client asks for a window, Spring builds a pageable request, the database returns just that window, and Spring optionally counts the full result set.
Real-World Story: Imagine an e-commerce product catalog service that powers the search page for a shopping app. The frontend shows 24 products per screen and requests /api/products?page=0&size=24&sort=createdAt,desc. The backend uses Spring Data pagination so the first screen loads fast, the “Next” button is cheap, and the UI can display “Showing 1–24 of 18,432”.
Now the bug: a developer assumed page numbers were 1-based and subtracted one in the controller, while the frontend already sent 0-based values. The first request became page -1 or mapped to the wrong offset, depending on the fix. Users reported duplicate products, missing first-page results, and a “Load more” button that seemed to jump around. In logs, the team saw repeated SQL like offset 24 on what was supposed to be the initial request, plus spikes in 400 Bad Request responses when size validation was added late.
What went wrong mattered because search pages are one of the highest-traffic endpoints in the system. A small pagination mistake turned into a visible UX problem, more support tickets, and a latency spike when the count query hit a huge joined table. The lesson: pagination is not just a controller detail; it is part of your data contract.
package com.example.paginationdemo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.util.stream.IntStream;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
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;
import org.springframework.web.server.ResponseStatusException;
@SpringBootApplication
public class PaginationApplication implements CommandLineRunner {
private final ProductRepository productRepository;
public PaginationApplication(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public static void main(String[] args) {
SpringApplication.run(PaginationApplication.class, args);
}
@Override
public void run(String... args) {
// Seed data so you can hit the endpoint immediately and see several pages.
if (productRepository.count() == 0) {
IntStream.rangeClosed(1, 23).forEach(i ->
productRepository.save(new Product("Product " + i, BigDecimal.valueOf(i * 9.99)))
);
}
}
}
@Entity
@Table(name = "products")
class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
protected Product() {
// JPA needs a no-args constructor.
}
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
}
record ProductDto(Long id, String name, BigDecimal price) {
static ProductDto from(Product product) {
return new ProductDto(product.getId(), product.getName(), product.getPrice());
}
}
interface ProductRepository extends JpaRepository<Product, Long> {
}
@RestController
@RequestMapping("/api/products")
class ProductController {
private final ProductRepository productRepository;
ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@GetMapping
public Page<ProductDto> listProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "5") int size) {
// Page numbers are zero-based in Spring Data by default.
if (page < 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "page must be >= 0");
}
// A hard cap protects the API from huge requests and accidental abuse.
if (size < 1 || size > 50) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "size must be between 1 and 50");
}
Pageable pageable = PageRequest.of(page, size, Sort.by("id").ascending());
Page<Product> result = productRepository.findAll(pageable);
// If the client asks for a page past the end, Spring Data returns an empty page.
// That is useful because the API stays stable instead of crashing.
return result.map(ProductDto::from);
}
}
@Component
class SampleDataNote {
// This class is intentionally empty.
// It keeps the example in one file and reminds you that the data comes from the CommandLineRunner.
}Follow-up & Tricky Questions:
Pageable from the request? Spring MVC reads query parameters like page, size, and sort and turns them into a PageRequest. This works automatically in Spring Boot when Spring Data web support is on the classpath.Page and Slice? Page includes total counts and total pages, while Slice only tells you whether there is a next or previous chunk. Use Slice when you do not need totals and want to avoid the count query.spring.data.web.pageable.default-page-size; locally, use @PageableDefault on the controller method parameter. That lets you choose a reasonable default without forcing every client to send size.sort=createdAt,desc&sort=id,desc or build a Sort object in code. Multiple fields are important for stable ordering in changing data.1 or 0? By default in Spring Data it is 0. You can switch to one-based parameters, but if you do not configure that, assuming page 1 is first is a common bug.Page<T> always cost only one query? No. It usually costs one query for the data and another for the total count, which is exactly why Slice<T> can be faster.Common Mistakes:
page=0, or explicitly configure one-based parameters if your API contract needs them.id or createdAt, id so records do not jump between pages.spring.data.web.pageable.max-page-size so one request cannot pull back too much data.Memory Hook: Pagination is a library shelf: sort picks the aisle, page picks the shelf, and size says how many books you can carry.
Cheat Sheet:
Pageable is the request; Page<T> is the response.0; default size is usually 20.Page gives totals; Slice skips the count query.Practice Tasks:
sort query parameter and test name,desc and id,asc.size > 20 and return a clear 400.Page<ProductDto> with Slice<ProductDto> and observe the smaller metadata shape.