What are dynamic projections in Spring Data, and how does the generic <T> method parameter choose the result shape at call time?
answer
- Generic <T> + Class<T> argument
- One method, many shapes chosen at call time
- ResultProcessor picks proxy vs DTO vs entity
- Class<T> not part of the query predicate
- Open projection loses column optimization
basics
~20 sA dynamic projection is one query method that can return different shapes depending on a Class<T> argument you pass at call time. You write <T> List<T> findByLastname(String lastname, Class<T> type) and call it with the DTO, interface, or entity class you want.
solid answer
~40 sDynamic projections let a single repository method produce different result types chosen by the caller. You declare a generic type parameter and take a Class<T> argument: `<T> List<T> findByActive(boolean active, Class<T> type);`. At call time you pass `UserNameDto.class`, an interface projection, or the entity `User.class`, and Spring Data applies the corresponding projection to the same underlying query. This avoids writing near-duplicate methods per shape. The passed class can be a closed interface projection (optimized column selection), a class-based DTO, or the full entity. It works with derived queries and with @Query. The key mechanism: the ResultProcessor inspects the runtime Class<T> to decide whether to build a proxy (interface), instantiate a DTO (class), or return the entity — the query predicate stays the same, only the returned shape varies.
code
java · 9 linespublic interface UserRepository extends JpaRepository<User, Long> {
// One method, shape chosen by caller
<T> List<T> findByLastname(String lastname, Class<T> type);
}
// call sites
List<UserNameDto> dtos = repo.findByLastname("Doe", UserNameDto.class); // class DTO
List<NamesOnly> proj = repo.findByLastname("Doe", NamesOnly.class); // interface projection
List<User> full = repo.findByLastname("Doe", User.class); // entitygo deeper
Recognize the <T> ... find...(..., Class<T> type) pattern picks the return shape at call time.
Explain that one method serves DTO, interface, and entity shapes, and Class<T> is not a filter.
Discuss ResultProcessor/ProjectionFactory resolution and closed-vs-open column optimization tradeoffs.
Design repository surfaces around dynamic projections to keep them minimal; reason about where the shape decision belongs (caller vs repository) and performance under open projections.
**The motivation.** Without dynamic projections you'd write one method per output shape: `findByLastname` returning `User`, `findNameByLastname` returning `UserNameDto`, `findSummaryByLastname` returning an interface, etc. — all with the same WHERE clause. A **dynamic projection** collapses these into one generic method whose result shape is chosen by the caller. **How to declare it.** Add a generic type parameter `<T>` to the method and accept a `Class<T>` parameter: ```java interface UserRepository extends Repository<User, Long> { <T> List<T> findByLastname(String lastname, Class<T> type); <T> T findById(Long id, Class<T> type); <T> Page<T> findByActive(boolean active, Pageable p, Class<T> type); } ``` The `Class<T>` argument is a **special, recognized parameter** — Spring Data strips it out of the query derivation (it is not treated as a query criterion) and uses it purely to pick the projection. **How it resolves at call time.** Spring Data's `ResultProcessor` and `ProjectionFactory` examine the runtime class you pass: - Pass an **interface** (e.g. `NamesOnly.class`) → returns a proxy; if it's a *closed* projection (only accessor methods matching properties) the SQL selects only those columns; an *open* projection (with `@Value` SpEL) loads the whole entity. - Pass a **class/record DTO** (e.g. `UserNameDto.class`) → instantiates it via constructor, selecting matching columns (closed). - Pass the **entity** (e.g. `User.class`) → returns the managed entity as usual. **Call site:** ```java List<UserNameDto> names = repo.findByLastname("Doe", UserNameDto.class); List<NamesOnly> proj = repo.findByLastname("Doe", NamesOnly.class); List<User> full = repo.findByLastname("Doe", User.class); ``` Same method, three shapes. **Works with @Query too.** You can combine a dynamic projection with an explicit `@Query`. But note: with a hand-written JPQL selecting the full entity, passing a closed interface class still projects in memory but does not necessarily reduce the SELECT columns the way a derived closed projection does — column optimization is strongest for derived queries and closed interface projections. Test if column-level efficiency matters. **Constraints & gotchas.** - The `Class<T>` parameter must be the projection type; you cannot pass an arbitrary unrelated class — properties must be resolvable against the entity. - It cannot select columns that don't exist as entity properties (for closed projections). - Open interface projections defeat the column-selection optimization (whole entity loaded), so a 'dynamic' open projection is not cheaper than returning the entity. - The extra `Class<T>` parameter does not participate in the derived-query predicate — don't name a property 'type' and expect a clash; Spring Data recognizes the `Class` argument by type. - Return-type wrappers (`List<T>`, `T`, `Optional<T>`, `Page<T>`, `Stream<T>`) all support the `T` substitution. **When to use.** APIs or services that need the same lookup at different fidelity — a lightweight summary for a list view and the full entity for a detail/edit view — without duplicating query methods. It keeps the repository surface small and pushes the shape decision to the caller.
- Does the Class<T> parameter affect the query's WHERE clause?No. Spring Data recognizes the Class argument by type and excludes it from query derivation; it only selects the projection/result shape. The predicate comes from the method name (or @Query) and the other parameters.
- If you pass an open (@Value SpEL) interface projection to a dynamic projection method, is it more efficient than returning the entity?No. Open projections load the whole entity to evaluate the SpEL, so there is no column-selection benefit. Only closed projections (interface or DTO) let Spring Data restrict the SELECT.
- Which return wrappers support the generic T?T, Optional<T>, List<T>, Stream<T>, Page<T>, Slice<T> — the T substitution flows through the standard return-type wrappers.
saying these in an interview costs you the question
- Thinking Class<T> becomes a query filter/criterion
- Claiming open projections still cut columns via dynamic projection
- Believing you must write one method per DTO shape
- Passing a class with properties that don't exist on the entity and expecting it to work