Projections: fetching only what you need
Loading a whole entity when you only need two of its fields is wasteful — every column fetched, every relationship considered, the object tracked by the persistence context. Projections let you fetch exactly the fields you need, as a lighter result. For read-heavy endpoints (a list view, a dropdown, a report) projections are a real, measurable optimisation. This lesson covers the projection types, verified against a running Dakiya.
Why not always load the whole entity?
Loading a full entity has costs that add up on read-heavy paths:
- Every column is selected, including large text or fields you will not use.
- The entity is managed — the persistence context tracks it for dirty checking, using memory, even though you are only reading.
- Relationships may trigger extra work (lazy proxies, or eager loads).
For an endpoint that returns a list of parcels showing only the tracking code and destination — a common
case — loading full Parcel entities fetches far more than the response needs. A projection fetches only
the columns the response uses, from a simpler query. The rule: for read-only endpoints that use a few
fields, project; load the whole entity when you will modify it (so dirty checking works).
Interface projections: the simplest
Declare an interface with getters for exactly the fields you want, and a repository method returning it:
public interface ParcelSummary {
String getTrackingCode();
String getDestinationCity();
}
public interface ParcelRepository extends JpaRepository<Parcel, Long> {
List<ParcelSummary> findSummaryByDestinationCity(String city); // returns projections, not entities
}
Spring Data sees the return type is a projection interface and selects only the mapped columns
(tracking_code, destination_city) — not the whole row. Verified against the running app:
findSummaryByDestinationCity("Pune") returned 3 ParcelSummary objects, each exposing only
getTrackingCode() and getDestinationCity() — there is no getRecipientName() on the projection, because
that column was not fetched. The projection is a lightweight, read-only view of just the fields you named.
Interface projections also support nested projections (a getter returning another projection interface, for a related entity's fields) and derived closed vs open projections — but the flat, closed interface above covers the common case.
Class (DTO) projections
You can also project into a concrete class — often a record — via its constructor, with a JPQL constructor expression:
public record ParcelSummaryDto(String trackingCode, String destinationCity) {}
@Query("select new com.riztech.dakiya.ParcelSummaryDto(p.trackingCode, p.destinationCity) from Parcel p")
List<ParcelSummaryDto> summaries();
The select new <FQN>(...) constructs the record directly from the selected columns — again fetching only
those columns. Class projections give you a concrete, immutable object (a record) you control, which is
handy when the projection is also your API response DTO. Interface projections need less code (no query, no
class body); class/record projections give you a real object with methods if you need them. Both fetch only
the named columns; pick by whether you want the zero-boilerplate interface or a concrete record.
Projections and the API layer
Projections and DTOs (the REST module) work together: a projection is how you fetch a subset from the
database; a DTO is what you expose at the API. Often they are the same shape, and a record projection can
serve as both — the query fetches exactly the response fields, and you return it directly. Other times you
project to a lean read model and still map to a separate API DTO. Either way, the point is the same as
DTOs': do not ship whole entities, and do not fetch more than you need. A list endpoint backed by a
projection selects a few columns and returns them — fast, lean, and with no lazy-loading surprises (a
projection is not a managed entity with lazy proxies, so it sidesteps LazyInitializationException too).
When projections are worth it — and when not
Be proportionate:
- Worth it: read-heavy list endpoints, dropdowns, reports, and any query returning many rows where you use a few fields. Fewer columns and no entity tracking is a genuine saving at scale.
- Not worth it: when you need the whole entity anyway (you will modify it, or you use most fields). Projecting then would just add a type for no gain, and you cannot modify a projection (it is read-only — no dirty checking).
- The measure: as with all performance work, do not project speculatively everywhere. Use whole entities by default; reach for a projection where a read-heavy query fetches far more than the response uses. The performance lesson closes this module with how to see that.
The habit to carry: fetch the narrowest shape the caller needs — a projection for read-only subsets, a full entity when you will change it. It is the query-level counterpart to returning DTOs at the API: both refuse to move more data than necessary.
Check your work
Why project. A full entity fetches every column, is tracked by the persistence context, and may trigger relationship work; a projection fetches only the fields you name — lighter, for read-only subsets.
Interface projections. An interface of getters for the wanted fields, returned by a repository method; Spring Data selects only those columns. Verified: the summary projection exposed only trackingCode + destinationCity (no recipientName — that column was not fetched).
Class/record projections. JPQL select new <FQN>(...) builds a record from selected columns — a
concrete, immutable object you control. Interface = less code; record = a real object (and can double as the
API DTO).
With the API. Projection = how you fetch a subset; DTO = what you expose; often the same shape, and a record can serve both. Projections also sidestep lazy-loading surprises (not managed entities).
When. Read-heavy list/report queries using a few fields → project; whole entity when you will modify it or use most fields; do not project speculatively — measure.
Practice
- Add a
ParcelSummaryinterface projection and afindSummaryBy...method; confirm it returns only the projected fields (reproduce the verified result) and that show-sql selects only those columns. - Add a record projection with a
select new ...JPQL query; confirm it also fetches only those columns. - Compare the SQL for a full-entity query versus the projection for the same rows — count the columns selected.
- Try to modify a projection and confirm you cannot persist a change through it (read-only, no dirty checking).
- Use a record projection as both the query result and the API response DTO for a list endpoint.
- For three Dakiya read endpoints, decide whole entity vs projection and justify each.
Official documentation
- Spring Data — Projections — Interface and class projections.
- Hibernate — Constructor expressions —
select newDTO projections. - Vlad Mihalcea — The best way to fetch DTO projections — Projection performance in depth.
Next: dynamic queries with Specifications and Criteria.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship