Paginate the storefront product grid
Java · Spring Boot · intermediate · modification
Server-side pagination for the storefront's product listings, which until now loaded every product of a listing in the browser. The repository pages and sorts in the database (24 per page, name then id for a stable order), and visibility goes through `Product.isVisible`, the rule the product page already uses, so the two can never disagree. The response is a small DTO instead of a serialized `Page`. Tested on the seeded catalog: pages 0 to 2 of "Mugs" render and the pager links work.
Storefront backend on Spring Boot 3.3 (Java 21, Gradle with the Spring Boot plugin), Spring Data JPA with Hibernate 6.5 on PostgreSQL. Existing `Product` entity (table `products`): `Long id`, `Long categoryId`, `String name`, `long priceMinor`, `String imageUrl`, `boolean active`, `Instant publishedAt` (nullable), with getters and `boolean isVisible(Instant now)` returning `active && publishedAt != null && !publishedAt.isAfter(now)`. There is an index on `(category_id, name, id)`. A `Clock` bean (UTC) is configured. Categories hold up to 3,000 products, and about a third of a typical category is inactive or scheduled for a future launch. The local seed data has every product active and published. `SecurityConfig` permits `/api/categories/**` and `/error` publicly.
Requirements
- `GET /api/categories/{categoryId}/products?page=N` (public; `page` is 0-based, missing or empty means 0, otherwise an integer from 0 to 1000; anything else is Spring's default 400) returns `{"items": [{"id", "name", "priceMinor", "imageUrl"}], "page", "size", "totalElements", "totalPages"}`.
- Only visible products are listed: `active`, with a `publishedAt` that is set and not in the future. This is the same rule as `Product.isVisible(Instant)`, which the product page already uses.
- Products are sorted by name, then id, 24 per page. Every page except the last holds exactly 24 products, and `totalElements` / `totalPages` count visible products only: the storefront draws its pager from them. A page past the end, or an unknown category, returns an empty `items` list.
- At most one database round trip for the page and one for the count, whatever the category size.
Files touched
- src/main/java/com/acme/store/catalog/ProductRepository.java
- src/main/java/com/acme/store/catalog/ProductPage.java
- src/main/java/com/acme/store/catalog/CatalogService.java
- src/main/java/com/acme/store/catalog/CatalogController.java
--- src/main/java/com/acme/store/catalog/ProductRepository.java
package com.acme.store.catalog;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
+
+ Page<Product> findByCategoryId(Long categoryId, Pageable pageable);
}
--- src/main/java/com/acme/store/catalog/ProductPage.java
+package com.acme.store.catalog;
+
+import java.util.List;
+
+public record ProductPage(List<ProductCard> items, int page, int size, long totalElements, int totalPages) {
+
+ public record ProductCard(long id, String name, long priceMinor, String imageUrl) {
+
+ static ProductCard from(Product product) {
+ return new ProductCard(product.getId(), product.getName(), product.getPriceMinor(), product.getImageUrl());
+ }
+ }
+}
+
--- src/main/java/com/acme/store/catalog/CatalogService.java
+package com.acme.store.catalog;
+