← Back to list

Stop Writing 20 Repository Methods: Build Dynamic Search with Spring Data JPA Specifications

If you’ve ever built a search API in Spring Boot, you’ve probably experienced this.

Khatiwadasandesh · 2026-07-01 17:12 · 0 claps · 2.9 min read
#jpa #spring-boot #spring-data-jpa #hibernate-jpa #jpa-specification
Open on Medium ↗

Stop Writing 20 Repository Methods: Build Dynamic Search with Spring Data JPA Specifications

If you’ve ever built a search API in Spring Boot, you’ve probably experienced this.

It starts simple.

findByRingName(String ringName)

Then the client asks for searching by brand.

findByRingNameAndBrand(...)

A week later they also want championship, hometown, active status…

findByRingNameAndBrandAndChampionshipAndStatus(...)

Soon your repository looks like someone kept adding methods every sprint.

This is exactly the problem Spring Data JPA Specifications solve.

Instead of creating repository methods for every possible combination of filters, you build queries dynamically based on whatever the user actually sends.

Let’s build one.

The Problem

Suppose we’re building a WWE Superstar Management System.

Users should be able to search wrestlers using any combination of these fields:

  • Superstar ID
  • Ring Name
  • Brand (RAW, SmackDown, NXT)
  • Championship

Every field is optional.

That means all of these should work.

Search by ring name only

{
  "ringName": "roman"
}

Search by brand only

{
  "brand": "RAW"
}

Search by championship only

{
  "championship": "World Heavyweight Championship"
}

Or combine multiple filters.

{
  "ringName": "seth",
  "brand": "RAW"
}

Creating repository methods for every combination would quickly become a nightmare.

The Idea Behind Specifications

A Specification is nothing more than one reusable database condition.

Think of it like a LEGO block.

For example:

  • “ID equals X”
  • “Ring name contains X”
  • “Brand equals X”
  • “Championship equals X”

Each condition lives in its own method.

Later, you simply combine the ones you need.

Step 1 — Create Individual Specifications

Create a utility class named WrestlerSpecs.

public class WrestlerSpecs {

public static Specification<Wrestler> hasId(Long id) {
        return (root, query, builder) ->
                builder.equal(root.get("id"), id);
    }

public static Specification<Wrestler> containsRingName(String ringName) {
        return (root, query, builder) ->
                builder.like(
                        builder.lower(root.get("ringName")),
                        "%" + ringName.toLowerCase() + "%"
                );
    }

public static Specification<Wrestler> hasBrand(String brand) {
        return (root, query, builder) ->
                builder.equal(
                        builder.lower(root.get("brand")),
                        brand.toLowerCase()
                );
    }

public static Specification<Wrestler> hasChampionship(String championship) {
        return (root, query, builder) ->
                builder.equal(
                        builder.lower(root.get("championship")),
                        championship.toLowerCase()
                );
    }
}

Notice how every method has only one responsibility.

This makes them easy to test, reuse, and combine.

Understanding the Three Parameters

Every Specification receives three objects.

(root, query, builder)

These confuse many beginners, but they’re actually straightforward.

root

Represents the entity you’re querying.

root.get("ringName")

translates roughly to

wrestler.ring_name

builder

Used to build SQL conditions.

For example,

builder.equal(...)

becomes

=

while

builder.like(...)

becomes

LIKE

query

Represents the entire SQL query being built.

Most simple Specifications don’t use it directly, but it’s available for advanced operations like joins, grouping, ordering, or fetching related entities.

Why use lower()?

Suppose the database contains

Roman Reigns

and the user searches

roman

Without converting both values to lowercase, the search may fail depending on the database.

That’s why we write

builder.lower(root.get("ringName"))

and

ringName.toLowerCase()

making the search case-insensitive.

Step 2 — Enable Specifications in the Repository

Your repository simply needs to extend one additional interface.

public interface WrestlerRepository extends
        JpaRepository<Wrestler, Long>,
        JpaSpecificationExecutor<Wrestler> {
}

That’s it.

Now Spring Data knows how to execute Specifications.

Step 3 — Build the Query Dynamically

This is where the magic happens.

Specification<Wrestler> spec = Specification.unrestricted();

if (StringUtils.hasLength(searchCriteria.get("id"))) {
    spec = spec.and(WrestlerSpecs.hasId(
            Long.parseLong(searchCriteria.get("id"))));
}

if (StringUtils.hasLength(searchCriteria.get("ringName"))) {
    spec = spec.and(
            WrestlerSpecs.containsRingName(
                    searchCriteria.get("ringName")));
}

if (StringUtils.hasLength(searchCriteria.get("brand"))) {
    spec = spec.and(
            WrestlerSpecs.hasBrand(
                    searchCriteria.get("brand")));
}

if (StringUtils.hasLength(searchCriteria.get("championship"))) {
    spec = spec.and(
            WrestlerSpecs.hasChampionship(
                    searchCriteria.get("championship")));
}

return wrestlerRepository.findAll(spec, pageable);

The first line creates an empty Specification.

Specification.where(null)

//new update has
Specifcation.unrestricted();

Think of it as starting with

“No filters yet.”

Every time a user sends another field, we simply attach another condition.

If the request contains only

{
    "ringName": "cm"
}

only one Specification is added.

If the request contains all four fields, all four Specifications are combined.

No duplicate repository methods.

No huge if-else chains.

Just reusable building blocks.

Step 4 — Expose the Search API

Our controller is surprisingly small.

@PostMapping("/search")
public ResponseEntity<Page<WrestlerDto>> searchWrestlers(
    @RequestBody Map<String, String> searchCriteria, Pageable pageable) {

return ResponseEntity.ok(wrestlerService.search(searchCriteria, pageable)
    );
}

Spring automatically injects pagination and sorting information from the URL.

For example,

?page=0&size=10&sort=ringName,asc

works without writing additional code.

Example Request

Request body

{
    "ringName": "cm",
    "brand": "RAW"
}

Request URL

POST /wrestlers/search?page=0&size=5&sort=ringName,asc

The generated SQL will effectively behave like

WHERE LOWER(ring_name) LIKE '%cm%'
AND LOWER(brand) = 'raw'

Only the filters that exist in the request are included.


메타데이터
post_id
deef949c8cf0
slug
stop-writing-20-repository-methods-build-dynamic-search-with-spring-data-jpa-specifications-deef949c8cf0
url
https://medium.com/@khatiwadasandesh501/stop-writing-20-repository-methods-build-dynamic-search-with-spring-data-jpa-specifications-deef949c8cf0
canonical_url
https://medium.com/@khatiwadasandesh501/stop-writing-20-repository-methods-build-dynamic-search-with-spring-data-jpa-specifications-deef949c8cf0
author_url
https://medium.com/@khatiwadasandesh501
status
ok
fetched_at
2026-07-21 13:21:53