Use Case Overview

Imagine you’re building an Product Catalog Service — the core API that powers your online store.

Your front-end team wants:

As a backend engineer, your job is to design an efficient REST API that:

A Spring Boot REST API that exposes /api/productsendpoint.
This endpoint allows clients (UI, mobile, or other APIs) to:

Why we need Pagination, Filtering, and Sorting

Let’s say your product table has 100,000 products.

If you call /api/products without pagination:

Pagination → returns results in pages, e.g. /api/products?page=2&size=10
Sorting → lets you order results, e.g. /api/products?sort=price,desc
Filtering → lets you focus results, e.g. /api/products?category=Electronics&brand=Samsung

You decide to use Java Spring Boot with Spring Data JPA for this — since it gives you built-in power to handle all three.

Solution Architecture

Here’s how the system fits together:

Layer

Responsibility

Entity (Product)

Defines product data model

Repository

Connects to DB and handles queries

Service

Implements filter + pagination logic

Controller

Exposes API endpoint

DTO (Paged Response)

Wraps paginated JSON results

H2 Database

Lightweight test data storage

Step 1: Setup Your Spring Boot Project

Create a Spring Boot project (you can use Spring Initializer) with:

pom.xml

<dependencies>
              <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-starter</artifactId>
              </dependency>
 
              <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-starter-test</artifactId>
                     <scope>test</scope>
              </dependency>
              <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
       </dependencies>

Step 2: Configure Your Database

application.properties
============= H2 Config =============
spring.datasource.url=jdbc:h2:mem:productdb 
spring.datasource.driverClassName=org.h2.Driver 
spring.datasource.username=sa 
spring.datasource.password= spring.h2.console.enabled=true
============= JPA / Hibernate =============
spring.jpa.hibernate.ddl-auto=create 
spring.jpa.defer-datasource-initialization=true
============= SQL Initialization =============
spring.sql.init.mode=always

Step 3: Create the Product Entity

Our API will serve data for a Product object with fields like ID, name, category, brand, and price.

import jakarta.persistence.*;
import lombok.*;
 
@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class product {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String name;
 
    private String category;
 
    private double price;
 
    private String brand;
    
 // Constructors
    public product() {}
 
    public product(Long id, String name, String category, String brand, double price) {
        this.id = id;
        this.name = name;
        this.category = category;
        this.brand = brand;
        this.price = price;
    }
 
    // Getters & Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
 
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
 
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
 
    public String getBrand() { return brand; }
    public void setBrand(String brand) { this.brand = brand; }
 
    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
}

Step 4: Define the Repository

This interface handles all DB communication.
Spring Data JPA auto-generates methods based on the name pattern.

import com.example.demo.entity.product;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface ProductRepository extends JpaRepository<product, Long> {
 
    // Filter by category
       Page<product> findByCategoryContainingIgnoreCase(String category, Pageable pageable);
    Page<product> findByBrandContainingIgnoreCase(String brand, Pageable pageable);
}

**Why this matters: \ These methods let you query by category or brand dynamically, without writing SQL.

Step 5: Implement the Service Layer

This layer applies business logic — i.e., which filter to apply and how to paginate.

import com.example.demo.entity.product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
 
@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;
 
    public Page<product> getAllProducts(String category, String brand, Pageable pageable) {
        if (category != null && !category.isEmpty()) {
            return productRepository.findByCategoryContainingIgnoreCase(category, pageable);
        } else if (brand != null && !brand.isEmpty()) {
            return productRepository.findByBrandContainingIgnoreCase(brand, pageable);
        } else {
            return productRepository.findAll(pageable);
        }
    }
}

Step 6: Build the Controller (Main API Endpoint)

This endpoint:

Step 7: Create a Paged Response DTO

We wrap our paginated results in a DTO for consistent JSON structure.

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
 
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PagedResponse<T> {
    private List<T> content;
    private int pageNumber;
    private int pageSize;
    private long totalElements;
    private int totalPages;
    private boolean last;
    
    public PagedResponse(List<T> content,
            int pageNumber,
            int pageSize,
            long totalElements,
            int totalPages,
            boolean last) {
this.content = content;
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.last = last;
}
 
// Getters and setters
public List<T> getContent() { return content; }
public void setContent(List<T> content) { this.content = content; }
public int getPageNumber() { return pageNumber; }
public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; }
public int getPageSize() { return pageSize; }
public void setPageSize(int pageSize) { this.pageSize = pageSize; }
public long getTotalElements() { return totalElements; }
public void setTotalElements(long totalElements) { this.totalElements = totalElements; }
public int getTotalPages() { return totalPages; }
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
public boolean isLast() { return last; }
public void setLast(boolean last) { this.last = last; }
}

Step 8: Initialize Sample Data Automatically

Instead of manually inserting data, let’s pre-load it when the app starts.

import com.example.demo.entity.product;
import com.example.demo.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
 @Component
public class DataInitializer implements CommandLineRunner {
     private final ProductRepository repository;
     public DataInitializer(ProductRepository repository) {
        this.repository = repository;
    }
 
    @Override
    public void run(String... args) {
        repository.save(new product(null, "iPhone 15", "Mobile", "Apple", 999.99));
        repository.save(new product(null, "Galaxy S23", "Mobile", "Samsung", 899.99));
        repository.save(new product(null, "MacBook Air", "Laptop", "Apple", 1299.00));
        repository.save(new product(null, "ThinkPad X1", "Laptop", "Lenovo", 1399.00));
        repository.save(new product(null, "OnePlus 12", "Mobile", "OnePlus", 749.99));
        repository.save(new product(null, "Sony Bravia 55\"", "TV", "Sony", 1099.00));
        repository.save(new product(null, "iPad Air", "Tablet", "Apple", 599.00));
    }
}

Step 9: Run and Test

Start your application:

Run As à Java Application

and start the Application, once the Application Started.

Now test the endpoints:

Description

URL

Get first page (3 items)

http://localhost:8080/api/products?page=0&size=3

Sort by price descending

http://localhost:8080/api/products?sort=price,desc

Filter by category

http://localhost:8080/api/products?category=Mobile

Filter by brand

http://localhost:8080/api/products?brand=Apple

Combine filter + sort

http://localhost:8080/api/products?category=Laptop&sort=price,asc

Sample Output

{
    "content": [
        {
            "id": 8,
            "name": "iPhone 15",
            "category": "Mobile",
            "price": 999.99,
            "brand": "Apple"
        },
        {
            "id": 9,
            "name": "Galaxy S23",
            "category": "Mobile",
            "price": 899.99,
            "brand": "Samsung"
        },
        {
            "id": 12,
            "name": "OnePlus 12",
            "category": "Mobile",
            "price": 749.99,
            "brand": "OnePlus"
        }
    ],
    "pageNumber": 0,
    "pageSize": 5,
    "totalElements": 3,
    "totalPages": 1,
    "last": true
} 


This gives the frontend everything it needs to build a table with pagination buttons:

Technical Insight (How Spring Does This)

Why This Matters (Real-World Context)

This same pattern is used in:

Conclusion

With this use case, you’ve mastered:

This setup forms the backbone of any enterprise API that needs to handle data efficiently while remaining scalable and maintainable.