Back to Projections: Interface, DTO, Dynamic
Q4Question 4 of 4

Interface, class/DTO constructor, dynamic generic projections?

Key Concepts
  • Interface projection declares getter methods and Spring builds a JDK proxy at runtime, narrowing the SQL for closed projections.
  • DTO/class projection uses a JPQL constructor expression or a Java record, producing plain unmanaged objects with no proxy overhead.
  • Dynamic projection takes a Class<T> parameter so one repository method returns different shapes at runtime.
  • Closed interface projections narrow the SQL to declared getters; open projections with @Value SpEL load the full entity first.
  • DTO projection is the fastest because it skips both the persistence context and the proxy layer.
  • Nested interface projections across associations can silently fire N+1 queries unless the underlying query uses JOIN FETCH.

Definition

Interface projection is a proxy over selected getters, DTO/class projection is a constructor expression or record (fastest), and dynamic projection takes a Class<T> so the caller chooses the shape at runtime.
  • Interface projection – a plain Java interface with getter methods matching entity properties; Spring Data builds a JDK proxy backed by only those columns.
  • Closed projection – an interface projection where every getter maps directly to a property, allowing SQL column narrowing.
  • Open projection – an interface projection using @Value SpEL that can reference any property, forcing Hibernate to load the full entity first.
  • DTO/class projection – a JPQL constructor expression (SELECT new ...) or a Java record that produces an immutable, unmanaged object.
  • Dynamic projection – a generic repository method with a Class parameter so the caller picks the shape at runtime.
  • Performance ranking – DTO (fastest, no proxy, no PC) > closed interface (fast, small proxy cost) > full entity (slowest, managed, snapshots).

Rajesh asks National Bank of India for his fixed-deposit details. The interface projection is a clerk who photocopies only the two lines you asked for onto a slip. The DTO projection is a clerk who hands you a pre-printed receipt with exactly those fields already laid out. The dynamic projection is a clerk who asks which format would you like today and prints whichever one you name at the counter.

Understanding the Concepts

What is an interface-based projection?

An interface-based projection is the simplest to write. You declare an interface whose getter names match the entity property names, and Spring Data generates a JDK dynamic proxy populated with only those columns. For a closed projection (getters that map directly to properties), Spring Data rewrites the SQL SELECT to include only those columns.

JAVA
package com.bank.projection;

import java.math.BigDecimal;

// Closed interface projection: only two columns fetched
public interface FdSummary {
    String getFdId();
    BigDecimal getMaturityAmount();
}
JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.FixedDeposit;
import com.bank.projection.FdSummary;
import java.util.List;

public interface FixedDepositRepository extends JpaRepository<FixedDeposit, String> {
    List<FdSummary> findByAccountId(String accountId);
    // SQL: SELECT f.fd_id, f.maturity_amount FROM fixed_deposits f WHERE f.account_id = ?
}
TEXT
Expected SQL: SELECT f.fd_id, f.maturity_amount FROM fixed_deposits f WHERE f.account_id = 'SAV2001'
Result: List<FdSummary> proxies with only two columns, not managed entities.

How does a closed projection differ from an open projection?

A closed projection declares getters that map directly to entity properties. Spring Data can optimize the SQL to fetch only those columns. An open projection uses @Value with SpEL expressions, which can reference any part of the entity. Because SpEL can access any property, Spring Data must fetch the WHOLE entity first and then compute the expression, losing the column-narrowing benefit.

JAVA
package com.bank.projection;

import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Value;

// CLOSED: only fd_id and maturity_amount fetched
public interface FdSummary {
    String getFdId();
    BigDecimal getMaturityAmount();
}

// OPEN: loads the FULL entity, then computes SpEL
public interface FdDisplayView {
    @Value("#{target.fdId + ' - Rs. ' + target.maturityAmount}")
    String getDisplayLabel();
}

| Aspect | Closed Projection | Open Projection |
|---|---|---|
| SQL columns | Only declared getters | All columns (full entity) |
| SpEL | Not used | Used with @Value |
| Performance | Fast (narrow SQL) | Slow (full entity loaded) |
| Use case | Simple column selection | Computed display values |

TEXT
Closed SQL: SELECT f.fd_id, f.maturity_amount FROM fixed_deposits f WHERE ...
Open SQL:   SELECT f.* FROM fixed_deposits f WHERE ...  (all 12 columns loaded!)

What is a DTO/class projection?

A DTO/class projection uses a JPQL constructor expression to create plain Java objects directly from the query results. The constructor parameter order must match the SELECT clause exactly. Since Spring Data 3, you can also return a Java record and Spring Data maps column-by-column automatically. DTO projections never enter the persistence context, so there is no proxy overhead and no dirty-check snapshot.

JAVA
package com.bank.dto;

import java.math.BigDecimal;

public record FdDto(String fdId, BigDecimal maturityAmount) {}
JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.bank.model.FixedDeposit;
import com.bank.dto.FdDto;
import java.util.List;

public interface FixedDepositRepository extends JpaRepository<FixedDeposit, String> {

    @Query("SELECT new com.bank.dto.FdDto(f.fdId, f.maturityAmount) " +
           "FROM FixedDeposit f WHERE f.status = :status")
    List<FdDto> findDtoByStatus(@Param("status") String status);
}
TEXT
Expected SQL: SELECT f.fd_id, f.maturity_amount FROM fixed_deposits f WHERE f.status = 'ACTIVE'
Result: List<FdDto> records -- immutable, no proxy, no persistence context.

What is a dynamic projection?

A dynamic projection uses a generic type parameter and a Class argument on the repository method. The caller decides at runtime which shape to return. One method signature serves interface projections, DTO records, and full entities.

JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.FixedDeposit;
import java.util.List;

public interface FixedDepositRepository extends JpaRepository<FixedDeposit, String> {
    <T> List<T> findByAccountId(String accountId, Class<T> type);
}
JAVA
package com.bank.service;

import com.bank.repository.FixedDepositRepository;
import com.bank.projection.FdSummary;
import com.bank.dto.FdDto;
import com.bank.model.FixedDeposit;
import java.util.List;

public class FdService {

    private final FixedDepositRepository fdRepo;

    public FdService(FixedDepositRepository fdRepo) {
        this.fdRepo = fdRepo;
    }

    public void demonstrate() {
        // Lightweight interface proxy
        List<FdSummary> summaries = fdRepo.findByAccountId("SAV2001", FdSummary.class);

        // Full managed entity
        List<FixedDeposit> full = fdRepo.findByAccountId("SAV2001", FixedDeposit.class);
    }
}
TEXT
With FdSummary.class: SELECT f.fd_id, f.maturity_amount FROM fixed_deposits f WHERE ...
With FixedDeposit.class: SELECT f.* FROM fixed_deposits f WHERE ... (full entity)

How do the three projection forms compare?

| Aspect | Interface (closed) | DTO/Class | Dynamic |
|---|---|---|---|
| SQL columns | Only declared getters | Only constructor params | Depends on Class |
| Return type | JDK Proxy | Record / plain object | Varies |
| Persistence context | Not managed | Not managed | Only if full entity |
| Dirty checking | No | No | Only if full entity |
| Proxy overhead | Small | None | Varies |
| Immutability | Read-only interface | Record (immutable) | Varies |
| Performance rank | 2nd | 1st (fastest) | Depends |

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bank.repository.FixedDepositRepository;
import com.bank.projection.FdSummary;
import com.bank.dto.FdDto;
import java.util.List;

@Service
public class FdDashboardService {

    private final FixedDepositRepository fdRepo;

    public FdDashboardService(FixedDepositRepository fdRepo) {
        this.fdRepo = fdRepo;
    }

    @Transactional(readOnly = true)
    public List<FdDto> getDtoProjection(String status) {
        return fdRepo.findDtoByStatus(status);  // FASTEST: no proxy, no PC
    }

    @Transactional(readOnly = true)
    public List<FdSummary> getInterfaceProjection(String accountId) {
        return fdRepo.findByAccountId(accountId);  // FAST: proxy, no PC
    }
}

What are common mistakes with projections?

The most common mistake is using an open interface projection and assuming it narrows the SQL. Open projections with @Value SpEL load the full entity. A second mistake is using nested interface projections for associations, which silently fires an extra query per row.

JAVA
// WRONG: nested projection causes N+1
package com.bank.projection;

public interface FdWithAccount {
    String getFdId();
    AccountView getAccount();  // nested projection -> extra query per FD!

    interface AccountView {
        String getAccountId();
        String getHolderName();
    }
}
JAVA
// CORRECT: flat projection with explicit JOIN FETCH in the query
package com.bank.projection;

import java.math.BigDecimal;

public interface FdFlatView {
    String getFdId();
    BigDecimal getMaturityAmount();
    String getAccountHolderName();  // flat: Spring maps to account.holderName
}

When should you use each projection form?

Use closed interface projections for simple internal screens where you need a few columns with minimal code. Use DTO/record projections for API responses where you want an explicit, immutable contract and the best performance. Use dynamic projections when one endpoint must serve both a summary view and a detail view from the same query.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bank.repository.FixedDepositRepository;
import com.bank.projection.FdSummary;
import com.bank.model.FixedDeposit;
import java.util.List;

@Service
public class FdSearchService {

    private final FixedDepositRepository fdRepo;

    public FdSearchService(FixedDepositRepository fdRepo) {
        this.fdRepo = fdRepo;
    }

    @Transactional(readOnly = true)
    public <T> List<T> search(String accountId, Class<T> type) {
        // Dynamic: caller picks FdSummary.class for list, FixedDeposit.class for detail
        return fdRepo.findByAccountId(accountId, type);
    }
}

What should a senior engineer know?

A senior engineer knows that DTO projections are the default for read endpoints because they skip both the persistence context and the proxy layer. They know that native queries can map to interface projections by column alias. They know that nested projections across associations trigger N+1 unless the query uses JOIN FETCH. They pair projections with @Transactional(readOnly=true) for maximum performance because neither dirty checking nor entity snapshots occur.

JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.bank.model.FixedDeposit;
import com.bank.dto.FdMaturityReport;
import java.util.List;

public interface FixedDepositRepository extends JpaRepository<FixedDeposit, String> {

    // DTO projection with aggregation -- senior-level pattern
    @Query("SELECT new com.bank.dto.FdMaturityReport(f.accountId, SUM(f.maturityAmount), COUNT(f)) " +
           "FROM FixedDeposit f GROUP BY f.accountId")
    List<FdMaturityReport> getMaturityReport();
}
JAVA
package com.bank.dto;

import java.math.BigDecimal;

public record FdMaturityReport(String accountId, BigDecimal totalMaturity, long fdCount) {}
TEXT
Expected SQL: SELECT f.account_id, SUM(f.maturity_amount), COUNT(f.fd_id)
             FROM fixed_deposits f GROUP BY f.account_id
Result: Aggregated report per account with total maturity and FD count.

Visual Overview

TEXT
  Three Projection Forms
  ============================================

  1. Interface Projection (Closed)
     Repository method returns: List<FdSummary>
     +---------------------------+
     | SELECT fd_id, maturity_amt|  <- only declared getters
     |           |               |
     |           v               |
     |    JDK Dynamic Proxy      |  <- Spring builds at runtime
     |    implements FdSummary   |
     |    (not managed)          |
     +---------------------------+

  2. DTO/Class Projection
     Repository method returns: List<FdDto>
     +---------------------------+
     | SELECT fd_id, maturity_amt|  <- constructor params
     |           |               |
     |           v               |
     |    new FdDto(id, amount)  |  <- plain record
     |    (no proxy, no PC)      |
     +---------------------------+

  3. Dynamic Projection
     Repository method: <T> List<T> findBy(..., Class<T>)
     +---------------------------+
     |  Class<T> type parameter  |
     |           |               |
     |    FdSummary.class?       |---> Interface proxy (narrow SQL)
     |    FdDto.class?           |---> Record object (narrow SQL)
     |    FixedDeposit.class?    |---> Full entity (SELECT *)
     +---------------------------+

  Performance: DTO > Interface (closed) > Entity

Key Takeaway

JPA projections let Rajesh fetch only the data a screen actually needs instead of hydrating a full entity, and Spring Data gives three forms. The interface-based projection is a plain Java interface with getters; Spring Data builds a proxy and, for a closed projection, narrows the SQL to just those columns. The DTO projection uses a JPQL constructor expression or a record return type; this is the fastest form because the objects never enter the persistence context and there is no proxy. The dynamic projection declares a single finder with a generic type parameter and a Class argument, so the caller decides at runtime whether to get a lightweight interface, a DTO, or the full entity from one method.

In the fixed-deposit portal at National Bank of India, the summary list uses a closed interface projection that reads only the deposit id and maturity amount, two columns instead of twelve. The detail API uses a DTO constructor projection so the JSON contract is explicit and there are no lazy-proxy serialization surprises. The search screen uses a dynamic projection so the same query powers both a compact row and a full drill-down depending on a toggle.

The performance ordering Pankaj keeps in mind is that the DTO projection is fastest because it reads the fewest columns and skips persistence-context management, the closed interface projection is close behind with a small proxy cost, and the full entity is slowest because every row carries a snapshot for dirty checking. The senior caution is that nested interface projections across associations can quietly trigger an extra query per row, so keep projections flat and move to a DTO whenever you need computed or joined values.

Remember one sentence: interface projections build proxies, DTO projections build plain records with no overhead, and dynamic projections let the caller choose the shape at runtime.

---

Share
Use ← → arrow keys to navigate