package com.example.catalog.web;
import com.example.catalog.domain.Product;
import java.math.BigDecimal;
public record ProductResponse(
Long id,
String name,
String category,
BigDecimal price) {
public static ProductResponse from(Product product) {
return new ProductResponse(
product.getId(),
product.getName(),
product.getCategory(),
product.getPrice());
}
}
package com.example.catalog.domain;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, length = 64)
private String category;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private boolean active = true;
protected Product() {
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public BigDecimal getPrice() {
return price;
}
public boolean isActive() {
return active;
}
}
package com.example.catalog.repository;
import com.example.catalog.domain.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.math.BigDecimal;
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("""
SELECT p FROM Product p
WHERE p.active = true
AND (:name IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :name, '%')))
AND (:category IS NULL OR p.category = :category)
AND (:minPrice IS NULL OR p.price >= :minPrice)
AND (:maxPrice IS NULL OR p.price <= :maxPrice)
""")
Page<Product> search(
@Param("name") String name,
@Param("category") String category,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice,
Pageable pageable);
}
package com.example.catalog.web;
import com.example.catalog.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository products;
public ProductController(ProductRepository products) {
this.products = products;
}
@GetMapping("/search")
public Page<ProductResponse> search(
@RequestParam(required = false) String name,
@RequestParam(required = false) String category,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@PageableDefault(size = 20, sort = "name", direction = Sort.Direction.ASC) Pageable pageable) {
return products
.search(name, category, minPrice, maxPrice, pageable)
.map(ProductResponse::from);
}
}
This snippet shows a focused slice of a Spring Boot service: a REST endpoint that returns a filtered, paginated page of products backed by a Spring Data JPA repository. The pattern is common whenever a catalog or list view needs server-side filtering and paging so the client never pulls the whole table into memory.
The Product entity maps a table with Jakarta Persistence annotations. It uses a Long surrogate key with GenerationType.IDENTITY, which delegates key generation to the database (a natural fit for Postgres serial/identity columns). The price field uses BigDecimal with an explicit precision and scale so money is stored exactly rather than as a lossy floating point value, and category is indexed implicitly through the filter query below.
In ProductRepository, the interface extends JpaRepository to inherit CRUD and paging. The key method, search, is a derived-ish @Query that accepts nullable filter parameters and short-circuits each predicate with (:name IS NULL OR ...). This idiom lets a single query serve any combination of filters — passing null simply disables that clause — which avoids the combinatorial explosion of writing one method per filter permutation. Crucially the method takes a Pageable and returns a Page<Product>; Spring Data automatically appends LIMIT/OFFSET and issues a second count query so the returned Page knows the total element count.
The ProductController wires it together. @ParameterizedRequest-style binding is avoided in favor of Spring's automatic Pageable argument resolution: @PageableDefault sets a sane default page size and sort so an unbounded request can't ask for the entire table. Filters arrive as optional @RequestParam values that default to null, matching the repository's null-tolerant query. The controller maps each Product to a ProductResponse DTO via map(ProductResponse::from) on the Page, keeping the JPA entity out of the serialized contract — this prevents accidental lazy-loading during JSON rendering and lets the API shape evolve independently of the schema.
The main trade-off is that the null-guard query relies on the planner to optimize dead branches; for very hot paths a Specification or Querydsl build would generate tighter SQL. For most catalog endpoints, though, this approach is readable, cacheable, and safe against unbounded result sets.
Related snips
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
Share this code
Here's the card — post it anywhere.