Pagination & Sorting
IntermediatePass a Pageable object to repository methods; Spring Data returns a Page<T> containing the slice of results plus total-count metadata.
Overview
Spring Data JPA pagination is built around Pageable (request) and Page<T> (response). A Pageable carries the page number (zero-based), page size, and Sort instructions; repositories automatically append LIMIT/OFFSET and a COUNT(*) query. Page<T> wraps the content slice plus metadata: totalElements, totalPages, number (current page), first/last flags. For large tables the COUNT(*) query can be expensive; use Slice<T> (no count) when you only need "has more pages" semantics for infinite scroll. Dynamic sorting from API parameters requires sanitisation — never pass raw field names from request parameters directly to Sort to prevent injection.
Repository pagination and controller wiring
Repository methods accept Pageable; Spring MVC resolves Pageable from request parameters automatically with @EnableSpringDataWebSupport.
// Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Spring Data generates: SELECT * FROM orders WHERE status=? LIMIT ? OFFSET ?
// + COUNT(*) query for totalElements
Page<Order> findByStatus(String status, Pageable pageable);
// Custom JPQL with pagination
@Query("SELECT o FROM Order o WHERE o.createdAt > :since")
Page<Order> findRecentOrders(@Param("since") LocalDateTime since,
Pageable pageable);
// Slice<T>: no count query — use for infinite scroll
Slice<Order> findByCustomerId(Long customerId, Pageable pageable);
}
// Controller — Spring resolves Pageable from ?page=0&size=20&sort=createdAt,desc
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public Page<OrderDto> list(
@RequestParam(defaultValue = "PENDING") String status,
@PageableDefault(size = 20, sort = "createdAt",
direction = Sort.Direction.DESC) Pageable pageable) {
return orderRepository.findByStatus(status, pageable)
.map(orderMapper::toDto);
}
}PageRequest and programmatic sorting
Construct PageRequest programmatically in service code when you need to control page size or sanitise sort fields.
@Service
public class OrderService {
private static final Set<String> SORTABLE_FIELDS = Set.of(
"createdAt", "total", "status"
);
public Page<OrderDto> search(OrderSearchRequest req) {
// Sanitise sort field — never trust raw user input
String sortField = SORTABLE_FIELDS.contains(req.getSortBy())
? req.getSortBy() : "createdAt";
Sort sort = Sort.by(
"asc".equalsIgnoreCase(req.getDirection())
? Sort.Direction.ASC : Sort.Direction.DESC,
sortField
);
// PageRequest is the concrete Pageable implementation
Pageable pageable = PageRequest.of(
Math.max(0, req.getPage()), // clamp negative page to 0
Math.min(100, req.getSize()), // cap page size at 100
sort
);
return orderRepository.findByStatus(req.getStatus(), pageable)
.map(orderMapper::toDto);
}
}
// Multiple sort columns
Pageable multiSort = PageRequest.of(0, 20, Sort.by(
Sort.Order.desc("status"),
Sort.Order.asc("createdAt")
));Page response shape and Slice for infinite scroll
Page<T> includes a COUNT query; Slice<T> skips it. For REST APIs, map Page<Entity> to a custom PageResponse DTO to control the response shape.
// Custom PageResponse DTO — avoids exposing Spring internals
@Value
public class PageResponse<T> {
List<T> content;
int page;
int size;
long totalElements;
int totalPages;
boolean first;
boolean last;
public static <T> PageResponse<T> from(Page<T> page) {
return new PageResponse<>(
page.getContent(),
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
);
}
}
// Controller returns custom DTO
@GetMapping
public PageResponse<OrderDto> list(@PageableDefault(size = 20) Pageable pageable) {
Page<OrderDto> page = orderRepository.findAll(pageable).map(orderMapper::toDto);
return PageResponse.from(page);
}
// Slice for infinite scroll (no count query → faster)
@GetMapping("/scroll")
public List<OrderDto> scroll(Pageable pageable) {
Slice<Order> slice = orderRepository.findByCustomerId(currentUser(), pageable);
// slice.hasNext() tells client whether to fetch next page
return slice.getContent().stream().map(orderMapper::toDto).toList();
}Key Points to Remember
- 1Spring MVC resolves Pageable automatically from ?page=0&size=20&sort=field,direction request parameters.
- 2Page<T> issues a COUNT(*) query for totalElements; Slice<T> skips it — use Slice for infinite scroll.
- 3Always sanitise sort field names — never pass raw user input to Sort to prevent field-name injection.
- 4Use @PageableDefault to set default page size and sort direction when no query parameters are provided.
- 5Map Page<Entity> to a custom DTO before returning from the controller — do not leak JPA proxy internals.
- 6For large offsets (page=10000), consider cursor-based pagination — OFFSET scans all preceding rows.
Interview Questions
Sign in to ask AriaWhat is the difference between Page<T> and Slice<T> in Spring Data?
How does Spring MVC resolve a Pageable parameter from HTTP request parameters?
What SQL queries does Spring Data execute for a Page<T> repository method?
Why is OFFSET-based pagination slow for large offsets and what is the alternative?
How would you prevent users from sorting by arbitrary database columns they should not see?
Ask Aria about Pagination & Sorting
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.