A focused example project demonstrating core QueryDSL capabilities with Spring Boot, Spring Data JPA, and PostgreSQL.
Build a query at runtime from optional filter parameters — only the conditions that are actually provided get added to the WHERE clause.
// CityService#search
BooleanBuilder predicate = new BooleanBuilder();
if (filter.name() != null) predicate.and(city.name.containsIgnoreCase(filter.name()));
if (filter.state() != null) predicate.and(city.state.equalsIgnoreCase(filter.state()));
if (filter.minPopulation() != null) predicate.and(city.population.goe(filter.minPopulation()));
if (filter.maxPopulation() != null) predicate.and(city.population.loe(filter.maxPopulation()));
return cityRepository.findAll(predicate, pageable);Endpoint: GET /cities?name=ber&minPopulation=500000&hasAirport=true
Inner-join a related collection and filter on a property of the joined entity.
// CityRepositoryCustomImpl#findCitiesWithPlaygroundPark
return queryFactory
.selectFrom(city)
.distinct()
.innerJoin(city.parks, park)
.where(park.hasPlayground.isTrue())
.fetch();Endpoint: GET /cities/with-playground-park
Group results, aggregate across related entities, and filter groups with a HAVING threshold.
// CityRepositoryCustomImpl#statsByState
return queryFactory
.select(Projections.constructor(StateStats.class,
city.state,
city.countDistinct(),
city.population.sumLong(),
hospital.numberOfBeds.avg()))
.from(city)
.leftJoin(city.hospitals, hospital)
.groupBy(city.state)
.having(city.countDistinct().goe(minCityCount))
.orderBy(city.state.asc())
.fetch();Returns StateStats(state, cityCount, totalPopulation, avgHospitalBeds).
Endpoint: GET /cities/stats-by-state?minCityCount=2
Select only the columns the caller needs — no full entity load, no N+1.
// CityRepositoryCustomImpl#findCitySummaries
return queryFactory
.select(Projections.constructor(CitySummary.class,
city.name, city.state, city.population))
.from(city)
.orderBy(city.name.asc())
.fetch();Returns CitySummary(name, state, population) instead of the full City entity graph.
Endpoint: GET /cities/summaries
Use JPAExpressions to write a correlated EXISTS subquery.
// CityRepositoryCustomImpl#findCitiesWithIcuHospital
return queryFactory
.selectFrom(city)
.where(
JPAExpressions.selectOne()
.from(hospital)
.where(hospital.city.eq(city).and(hospital.hasICU.isTrue()))
.exists())
.fetch();Endpoint: GET /cities/with-icu-hospital
src/main/java/.../querydsl/
├── domain/ # JPA entities — City, Park, Hospital, FireBrigade, Location
├── input/ # Request records — CityInput, CitySearchFilter, …
├── output/ # Response records — CitySummary, StateStats
├── repository/ # CityRepository (QuerydslPredicateExecutor)
│ # CityRepositoryCustom + CityRepositoryCustomImpl ← QueryDSL here
├── service/ # CityService — orchestration + BooleanBuilder logic
└── controller/ # CityController — REST endpoints
Q-types (QCity, QPark, …) are generated from JPA entities by the QueryDSL APT processor during mvn generate-sources.
compose.yaml starts both the PostGIS database and the application:
docker compose up --buildThe app is available at http://localhost:8080. The database is internal to the Compose network and not exposed to the host.
Credentials default to postgres/postgres via .env.docker. To use your own,
edit that file and pass it to Compose:
docker compose --env-file .env.docker up --build- Java 25, Spring Boot 4
- QueryDSL JPA 7.2 (openfeign fork)
- Spring Data JPA + Hibernate + PostGIS (geospatial
GEOGRAPHY POINT) - Liquibase migrations, Testcontainers for tests