Skip to main content

Command Palette

Search for a command to run...

Supercharge Spring Data JPA: Dynamic Filtering & Performance Optimization with Slices

Updated
3 min read

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:

  1. The actual data query (SELECT ... LIMIT ? OFFSET ?).

  2. 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:

  1. Cleaner Code: Zero boilerplate for dynamic filters.

  2. Better Performance: 50% fewer queries for infinite scroll views.

Check out the full implementation in the repository!

References

Spring

Part 1 of 1

In this series, I will explore hidden gems of the Spring and SpringBoot framework!