Querydsl is one of those libraries that everyone used for years and then quietly stopped updating. The 5.0 release has been "coming soon" since 2019. The GitHub shows commits but no milestone. The issue tracker has a thread titled "Is Querydsl dead?" with hundreds of comments.
It's not dead. But if you're starting a new project in 2026 and you're picking between Querydsl and something that's actively maintained, has Spring Boot 4 support, works with MongoDB and in-memory collections, generates OpenAPI docs automatically, and has companion frontend libraries... well, you see where I'm going.
This isn't a "Querydsl bad, Spring Filter good" article. Querydsl pioneered type-safe querying for Java and it deserves credit. But migrations happen, and if you're considering one, here's what the conversion looks like.
Side-by-side: basic filtering
Querydsl:
QCar car = QCar.car;
BooleanExpression filter = car.year.gt(2020)
.and(car.km.lt(50000))
.and(car.color.eq(Color.RED));
List<Car> results = new JPAQuery<>(entityManager)
.select(car)
.from(car)
.where(filter)
.fetch();
Spring Filter (query string):
@Filter Specification<Car> spec
// URL: ?filter=year > 2020 and km < 50000 and color : 'red'
List<Car> results = carRepo.findAll(spec);
Spring Filter (programmatic builder):
FilterNode filter = fb.field("year").greaterThan(fb.input(2020))
.and(fb.field("km").lessThan(fb.input(50000)))
.and(fb.field("color").equal(fb.input(Color.RED)))
.get();
Specification<Car> spec = converter.convert(filter);
List<Car> results = carRepo.findAll(spec);
Spring Filter (type-safe builder):
FilterNode f = CarFilter.where(fb)
.year().greaterThan(2020)
.and()
.km().lessThan(50000)
.and()
.color().equal(Color.RED)
.build();
Specification<Car> spec = converter.convert(f);
List<Car> results = carRepo.findAll(spec);
The type-safe builder reads almost exactly like Querydsl. The difference is that CarFilter is generated from your JPA entity via annotation processor, not from a separate Q-class that needs its own build step.
What Querydsl does that Spring Filter doesn't
Gotta be honest about this part.
1. SQL-level joins and subqueries.
Querydsl can express SELECT * FROM cars WHERE brand_id IN (SELECT id FROM brands WHERE country = 'DE'). Spring Filter doesn't generate subqueries. It handles entity relation traversal (brand.name : 'audi') which generates a JOIN under the hood, but complex subquery logic isn't in scope. If your query needs EXISTS (SELECT ...), you'll still need CriteriaBuilder or native SQL for that part.
2. Update and delete queries.
Querydsl has new JPAUpdateClause() and new JPADeleteClause(). Spring Filter is read-only. It generates WHERE clauses, not DML. If you need to bulk-update filtered records, you apply the generated Specification to find them, then update manually. Not as clean.
3. Tuple projections without mapping to DTOs.
Querydsl lets you select arbitrary expressions into Tuple objects. Spring Filter doesn't do projections at all -- it only generates WHERE clauses. Your SELECT is handled by Spring Data or your own CriteriaQuery setup. You can use Spring Filter's Specification in a projection query (the README shows how), but the projection itself is still CriteriaBuilder code.
4. Maturity of the type system.
Querydsl's APT processor handles generics, wildcards, and complex type hierarchies better than Spring Filter's @Filterable processor. If your entities use Map<Class<?>, List<? extends Serializable>> or similar, Querydsl generates correct Q-types. Spring Filter will skip or approximate these. For the 95% case (simple fields, enums, dates, basic collections), it's fine.
What Spring Filter does that Querydsl doesn't
1. MongoDB support with the same syntax.
Same filter string, same builder API, different converter:
// JPA
?filter=year > 2020 --> Specification<Car>
// MongoDB
?filter=year > 2020 --> Query (MongoDB query object)
One syntax across two databases. Querydsl has a MongoDB module but its syntax differs from the JPA module. Spring Filter's AST is database-agnostic.
2. In-memory predicate filtering.
No database at all:
@Filter Predicate<Car> predicate
// URL: ?filter=year > 2020
cars.stream().filter(predicate).collect(Collectors.toList());
This is useful for filtering cached data, testing, or filtering API responses after fetch. Querydsl has CollQuery for collections but it's more verbose and less commonly used.
3. Automatic OpenAPI/Swagger docs.
Add the openapi module and your Swagger UI automatically documents every filterable field, its type, available operators, and example queries. With Querydsl you're writing parameter docs by hand.
4. Frontend libraries that speak the same language.
FilterKit (the JS/TS sister library) uses the exact same filter syntax and AST. Your React frontend builds a query object, serializes it, sends it as a URL parameter, and your Spring backend parses it natively. No translation layer, no mapping, no mismatches. Querydsl has no frontend story.
5. ParseContext.
I keep talking about this in every article because it's the killer feature. ParseContext lets you intercept and rewrite filter expressions before they hit the database. Multi-tenancy, soft deletes, security filters, audit logging, field whitelisting -- all without touching your controllers. Querydsl's equivalent would be writing custom BooleanExpression wrappers per endpoint, which is more code and harder to enforce consistently.
Migration strategy: entity by entity
You don't have to rip out Querydsl in one commit. Spring Filter and Querydsl coexist fine. They use different dependency injection paths (Querydsl uses generated Q-types, Spring Filter uses @Filter annotations and converters). You can convert entities one at a time.
Step 1: Add the Spring Filter dependency alongside your existing Querydsl setup. No conflicts.
Step 2: Pick a low-traffic entity. Add @Filterable and wire up a controller with @Filter.
Step 3: Run both endpoints side by side (the old Querydsl one and the new Spring Filter one) for a release.
Step 4: Verify query results match. They should, since both generate JPA Criteria queries.
Step 5: Remove the Querydsl endpoint. Move to the next entity.
For the @Filterable part: you'll need the annotation processor in your compiler plugin. If you're already running Querydsl's APT processor, you just add a second processor path. Maven supports multiple annotation processors in the same plugin config.
The query syntax conversion cheat sheet
| Operation | Querydsl (JPAQuery) | Spring Filter (AST / URL) |
|---|---|---|
| Equals | car.year.eq(2020) |
year : 2020 |
| Not equals | car.year.ne(2020) |
year ! 2020 |
| Greater than | car.year.gt(2020) |
year > 2020 |
| Less than | car.year.lt(2020) |
year < 2020 |
| Between | car.year.between(2020, 2025) |
year between 2020 and 2025 |
| Like | car.name.like("%Audi%") |
name ~ '%Audi%' |
| In | car.brand.in("audi", "bmw") |
brand.name in ['audi', 'bmw'] |
| Is null | car.deletedAt.isNull() |
deletedAt is null |
| AND | .and(...) |
and |
| OR | .or(...) |
or |
| Join (relation) | car.brand.name.eq("audi") |
brand.name : 'audi' |
| Size | car.parts.size().gt(2) |
size(parts) > 2 |
The URL syntax is more readable to non-Java developers (frontend, QA, support). The builder syntax is readable to Java developers. Both map to the same AST.
When NOT to migrate
If your project:
- Uses Querydsl's SQL module (not JPA) extensively
- Has complex subqueries that can't be expressed as simple filters
- Is in maintenance mode and nobody's touching it
- Has a team that deeply knows Querydsl and has no interest in learning a new library
...then stay on Querydsl. Don't migrate for the sake of migrating.
If your project:
- Is actively developed and will be for years
- Has or wants a MongoDB component alongside JPA
- Has a React/Angular frontend that builds filter queries dynamically
- Needs automatic Swagger docs for filterable endpoints
- Wants to centralize cross-cutting concerns like multi-tenancy and soft deletes
...then the migration is worth it.
github.com/turkraft/springfilter. The JPA module is the most mature, MongoDB is close behind, and the predicate module is deceptively useful. The examples directory has working Spring Boot apps for all three.
Top comments (0)