Building a Cleaner Projection Layer on Top of JPA Criteria API

Published: (January 12, 2026 at 09:37 AM EST)
2 min read
Source: Dev.to

Source: Dev.to

Introduction

If you develop systems using Java with JPA, you have probably faced the need to execute queries that return only a subset of attributes from a given entity.
At first glance this may seem simple, but when not handled properly systems can accumulate unnecessary queries or queries overloaded with attributes that will never be used.

In many real‑world scenarios a developer needs to retrieve only the id and name of an entity. Because of the size and complexity of the system, it often becomes difficult to identify whether a query already exists that returns exactly this data. In other cases developers end up reusing methods that load the entire entity, only to later extract the few attributes that are actually required.

ProjectionQuery was created to simplify and organize projection‑based queries, providing a clearer and more expressive way to select only the data the application truly needs.

Defining a Projection

After adding the dependency to your project, create a class (or a record) and annotate the fields that should be projected.

@Projection(of = Customer.class)
public record CustomerBasicData(
        @ProjectionField Long id,
        @ProjectionField String name,
        @ProjectionField("address.city.name") String city,
        @ProjectionField("address.city.state.name") String state
) { }

This record represents the final shape of the query result. Regardless of how many attributes exist in the Customer entity, only id, name, city, and state will be selected from the database.
Note that city and state are nested attributes retrieved through relationships defined in the Customer entity.

Generated SQL (simplified)

The SQL generated for the above projection looks roughly like this:

SELECT
    c.id,
    c.name,
    city.name   AS city,
    state.name  AS state
FROM customer c
INNER JOIN address a   ON c.address = a.id
INNER JOIN city   ci  ON a.city = ci.id
INNER JOIN state  s   ON ci.state = s.id;

Executing Projections

Simplified execution

ProjectionProcessor processor = new ProjectionProcessor(entityManager);
List customers = processor.execute(CustomerBasicData.class);

Using ProjectionQuery for advanced scenarios

ProjectionQuery helps build more sophisticated queries, allowing the addition of filters, sorting, pagination, and other configurations.

ProjectionProcessor processor = new ProjectionProcessor(entityManager);

ProjectionQuery query = ProjectionQuery
    .fromTo(Customer.class, CustomerBasicData.class)
    .filter("address.city.name", ProjectionFilterOperator.EQUAL, "São Paulo")
    .order("name", OrderDirection.ASC)
    .paging(0, 20)
    .distinct();

List customers = processor.execute(query);

ProjectionQuery can be used both independently and integrated into Spring Boot applications.

Further Resources

For more details, additional examples, and full documentation, please refer to the project page on GitHub.

Back to Blog

Related posts

Read more »