Server‑Side Pagination for the Gardener Plant API with Spring Data JPA
Learn how to implement server‑side pagination for a Spring Boot Gardener API, avoid full‑table downloads, and keep responses fast and memory‑efficient.
31 Oct 2025, 22:25 UTC

The pain of sending every plant record at once
When a garden inventory contains thousands of Plant rows, a naïve GET /plants endpoint that returns the full list forces the client to download megabytes of JSON. This hurts network latency, overwhelms the server, and makes paging on the front‑end impossible.
Why server‑side pagination solves the problem
Server‑side pagination means the database does the heavy lifting: it returns only a slice of rows for each request. In Spring Data JPA this is expressed through the Pageable interface. The framework translates page, size and sort parameters into a JPA query with LIMIT and OFFSET (or native cursor) and wraps the result in a Page<Plant> object that carries metadata such as totalElements and totalPages. The client receives a manageable JSON payload and can request the next slice with a simple URL.
Step‑by‑step implementation
Domain model
@Entity
@Table(name = "plants")
public class Plant {
@Id @GeneratedValue
private Long id;
private String name;
private String species;
private String location;
// getters and setters omitted
}
The entity represents a single plant record and maps to a database table named plants. Primary key generation and basic fields are shown; additional columns can be added as needed.
Repository
public interface PlantRepository extends JpaRepository {
// Spring Data JPA already provides a findAll(Pageable pageable) method
}
The repository inherits the findAll(Pageable pageable) method, which automatically applies the pagination criteria supplied via a Pageable instance.
Controller
@RestController
@RequestMapping("/plants")
public class PlantController {
private final PlantRepository repo;
public PlantController(PlantRepository repo) {
this.repo = repo;
}
@GetMapping
public Page getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List sort) {
Sort sortObj = sort == null ? null : Sort.by(sort.stream()
.map(Sort.Order::valueOf)
.toList());
Pageable pageable = new PageRequest<>(page, size, sortObj);
return repo.findAll(pageable);
}
}
The controller method extracts query parameters, builds a Sort object if sorting is requested, creates a Pageable instance, and delegates to the repository. Spring Data JPA handles the translation to SQL.
Sample JSON response
Request: GET /plants?page=0&size=3&sort=name,asc
{
"content": [
{"id":1,"name":"Aloe","species":"Asphodelaceae","location":"North Bed"},
{"id":2,"name":"Basil","species":"Lamiaceae","location":"Herb Corner"},
{"id":3,"name":"Cactus","species":"Cactaceae","location":"South Rock"}
],
"pageable": {"sort": {"sorted":true,"unsorted":false,"empty":false},"pageNumber":0,"pageSize":3,"offset":0,"paged":true,"unpaged":false},
"totalPages": 45,
"totalElements": 1350,
"last": false,
"size": 3,
"number": 0,
"sort": {"sorted":true,"unsorted":false,"empty":true},
"first": true,
"numberOfElements": 3,
"empty": false
}
The response contains a content array with the requested slice, pagination metadata, and a totalElements count that matches the underlying table size.
Verification checklist
- Start the application with an embedded H2 database (or any JDBC datasource).
- Populate a few hundred
Plantrows using a script or the repository’ssaveAllmethod. - Run
curl -s "http://localhost:8080/plants?page=0&size=3"and inspect the response. - Confirm that
totalElementsmatches the count of rows,totalPagesequalsceil(totalElements/size), and thatcontentcontains exactlysizeitems (or fewer on the last page). - Repeat the request with
page=1and verify that theoffsetfield increments bysizeand that thefirstflag becomes false.
Trade‑offs and limitations
- Index requirement: If the columns used for sorting or filtering lack proper indexes, each page request may cause a full table scan, turning what should be a cheap slice into an expensive operation.
- Stale data visibility: Records inserted or deleted between two page requests can cause the same plant to appear on different pages, or a plant to disappear entirely. This is inherent to snapshot‑style paging.
- Memory pressure: Very large
sizevalues (e.g., 10 000) combined with complex queries can cause the JVM to allocate large result sets, risking out‑of‑memory errors.
Actionable closing
To get a production‑ready Gardener API:
- Choose a sensible page size (20‑100) and expose it as a query parameter with a maximum limit.
- Add database indexes on
name,speciesand any column used insortto keep pagination fast. - Consider using
COUNT(*) OVER ()or a separateCOUNTquery only when exact totals are required; otherwise rely onPage<Plant>’stotalElementsfor approximate sizing. - Implement client‑side caching of page metadata to reduce redundant requests when the user navigates back and forth.
Run the verification steps above after each change, and you’ll have a robust, efficient pagination strategy for the Gardener plant inventory API.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.