Supercharge Spring Data JPA: Dynamic Filtering & Performance Optimization with Slices
Building efficient search APIs in Spring Boot often leads to two common problems: infinite boilerplate code for dynamic filtering and performance bottlenecks caused by unnecessary count queries.
In this post, I'll share how we solved both using a Generic Specification Repository and Slice-based Pagination.
1. The Problem: Boilerplate & Heavy Queries
The Boilerplate Trap
Typically, allowing users to filter by name, status, createdDate, etc., requires writing endless custom repository methods or complex CriteriaBuilder logic.
The Count Query Killer
Standard pagination (Page<T>) executing findAll(Pageable) triggers two queries:
The actual data query (
SELECT ... LIMIT ? OFFSET ?).A total count query (
SELECT COUNT(*) ...).
For large tables, the count query is a performance killer. If you're building an "Infinite Scroll" or "Load More" feature, you don't even need the total count—you just need to know if there's a "next page".
2. The Solution: Generic Specification Repository
We implemented a wrapper around JpaSpecificationExecutor that accepts a dynamic SearchRequest. This allows us to filter any entity without writing custom query code.
The Interface
@NoRepositoryBean
public interface SpecificationRepository<T, ID> extends JpaRepository<T, ID>, SliceSpecificationExecutor<T> {
// Inherits findAll(Specification<T>, Pageable)
// Inherits findAllSliced(Specification<T>, Pageable)
}
The Usage
Now, any repository can extend this and instantly gain dynamic search powers:
public interface ThreatActorRepository extends SpecificationRepository<ThreatActor, UUID> {}
The Service layer simply builds the specification from a JSON request:
public Page<ThreatActor> search(SearchRequest request) {
Specification<ThreatActor> spec = GenericSpecificationBuilder.buildFromRequest(request);
return repository.findAll(spec, pageable);
}
3. The Optimization: Slice vs. Page
To solve the count query performance issue, we implemented SliceSpecificationExecutor.
What is a Slice?
A Slice in Spring Data holds a chunk of data and knows if there is a next slice (hasNext()), but it does not know the total number of pages.
Implementing findAllSliced
We leveraged Spring Data's Window API (introduced recently) to fetch results efficiently:
SliceSpecificationExecutor.javadefault Slice<T> findAllSliced(Specification<T> spec, Pageable pageable) { // Uses a "Window" to fetch size + 1 items to determine if a next page exists // completely avoiding the SELECT COUNT(*) query. Window<T> window = this.findBy(spec, ...); return new SliceImpl<>(window.getContent(), pageable, window.hasNext());}
The Performance Win
By switching from /search (Page) to /search-sliced (Slice), we eliminate the heavy count query entirely.
SQL Comparison:
Regular Page Request:
sqlHibernate: select ... from threat_actors limit ? offset ?Hibernate: select count(*) from threat_actors ... -- 🛑 EXPENSIVE on large tables
Slice Request:
sqlHibernate: select ... from threat_actors limit ? offset ? -- ✅ ONLY ONE QUERY
4. Conclusion
By combining the Specification Pattern with Slice Pagination, we achieved:
Cleaner Code: Zero boilerplate for dynamic filters.
Better Performance: 50% fewer queries for infinite scroll views.
Check out the full implementation in the repository!
