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