Back to Projections: Interface, DTO, Dynamic
Q1Question 1 of 4

What are JPA projections?

Key Concepts
  • JPA projections fetch only the columns you need instead of loading the entire entity with all its fields
  • Interface-based projection defines an interface with getter methods; Spring generates a proxy at runtime
  • DTO/class-based projection uses a JPQL constructor expression or a record – no proxy, direct object creation, fastest
  • Dynamic projection accepts Class<T> as a parameter, letting the caller choose the shape at runtime
  • DTO projection is the most performant: fewest columns, no proxy overhead, no persistence context management
  • Interface projection adds proxy overhead but supports SpEL @Value for computed fields
  • Full entity loads all columns and enters the persistence context with snapshot storage and dirty checking

Definition

JPA projections fetch only the columns your API needs – interface-based uses a proxy with getter methods, DTO-based uses a constructor expression (fastest), and dynamic lets the caller choose the projection at runtime.
  • Interface-based projection – define a Java interface with getter methods matching entity field names. Spring Data generates a proxy at runtime that maps only those columns from the SQL result.
  • DTO/class-based projection – use a JPQL constructor expression SELECT new Dto(e.field1, e.field2) or a Java record as the return type. Direct object creation with no proxy and no persistence context entry.
  • Dynamic projection – List findByStatus(String status, Class type) lets the caller choose the projection class at runtime.
  • Performance ranking – DTO is fastest (fewest columns, no proxy, no persistence context). Interface is second (few columns but proxy creation cost). Full entity is slowest (all columns, snapshots, dirty checking).

Loading the full Account entity is like the National Bank of India clerk photocopying Rajesh's entire 20-page customer file every time. Interface projection is like writing only his name and balance on a pre-printed slip. DTO projection is like printing a receipt with exactly the three fields the mobile app needs. Dynamic projection is like letting Rajesh choose which form he wants at the counter.

Understanding the Concepts

What are JPA projections?

JPA projections fetch only the columns you need instead of loading the entire entity. This reduces data transfer from the database, memory usage in the application, and eliminates persistence context overhead for read-only queries.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "trades")
public class Trade {
    @Id
    private String tradeId;
    private String symbol;
    private BigDecimal amount;
    private String status;
    private LocalDate settlementDate;
    private String notes;           // large TEXT field
    private String auditHistory;    // large TEXT field
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;
    // 8+ columns. The list API only needs tradeId, symbol, amount.
}

How does interface-based projection work?

Define an interface with getter methods for the fields you want. Spring Data generates a proxy at runtime that maps only those columns.

JAVA
package com.bank.dto;

import java.math.BigDecimal;

public interface TradeIdAndSymbol {
    String getTradeId();
    String getSymbol();
    BigDecimal getAmount();
}
JAVA
package com.bank.repository;

import com.bank.dto.TradeIdAndSymbol;
import com.bank.model.Trade;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface TradeRepository extends JpaRepository<Trade, String> {

    List<TradeIdAndSymbol> findByStatus(String status);
    // Generates: SELECT t.trade_id, t.symbol, t.amount
    //            FROM trades t WHERE t.status = ?
    // NOT: SELECT * FROM trades (all 8+ columns)
}
TEXT
SQL generated:
SELECT t.trade_id, t.symbol, t.amount FROM trades t WHERE t.status = 'SETTLED'
-- Only 3 columns fetched instead of 8+

How does DTO/class-based projection work?

DTO projection uses a JPQL constructor expression or a Java record as the return type. This is the most performant approach – no proxy, no entity management, direct object construction.

JAVA
package com.bank.dto;

import java.math.BigDecimal;

public record TradeOverview(String tradeId, String symbol, BigDecimal amount) {}
JAVA
package com.bank.repository;

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

public interface TradeRepository extends JpaRepository<Trade, String> {

    @Query("SELECT new com.bank.dto.TradeOverview(t.tradeId, t.symbol, t.amount) "
         + "FROM Trade t WHERE t.status = :status")
    List<TradeOverview> findOverviewByStatus(@Param("status") String status);
    // Result: List<TradeOverview> – plain records, no proxies,
    // no persistence context entry
}

How does dynamic projection work?

Dynamic projection lets the caller choose the projection class at runtime using a Class parameter.

JAVA
package com.bank.repository;

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

public interface TradeRepository extends JpaRepository<Trade, String> {

    <T> List<T> findByAccountId(String accountId, Class<T> type);
}
JAVA
package com.bank.service;

import com.bank.dto.TradeIdAndSymbol;
import com.bank.dto.TradeOverview;
import com.bank.model.Trade;
import com.bank.repository.TradeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class TradeDynamicService {

    private final TradeRepository tradeRepo;

    public TradeDynamicService(TradeRepository tradeRepo) {
        this.tradeRepo = tradeRepo;
    }

    @Transactional(readOnly = true)
    public void dynamicProjectionDemo() {
        // Same method, different projections!
        List<TradeIdAndSymbol> idsOnly =
            tradeRepo.findByAccountId("SAV2001", TradeIdAndSymbol.class);

        List<TradeOverview> overviews =
            tradeRepo.findByAccountId("SAV2001", TradeOverview.class);

        List<Trade> fullEntities =
            tradeRepo.findByAccountId("SAV2001", Trade.class);
    }
}

How is interface projection different from DTO projection?

| Feature | Interface Projection | DTO Projection |
|---------|---------------------|----------------|
| Definition | Interface with getters | Record or class with constructor |
| Implementation | Spring-generated proxy | Direct instantiation |
| SpEL @Value | Supported | Not supported |
| Persistence context | Not managed | Not managed |
| Nested projections | Supported (nested interfaces) | Not in constructor expression |
| Performance | Proxy overhead | No overhead (fastest) |
| Use case | Quick prototyping, computed fields | Production API responses |

JAVA
package com.bank.dto;

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

// Interface projection with SpEL computed field
public interface AccountSummary {
    String getAccountId();
    String getHolderName();
    BigDecimal getBalance();

    @Value("#{target.holderName + ' (' + target.accountId + ')'}")
    String getDisplayName();  // computed: "Rajesh Kumar (SAV2001)"
}
// SpEL @Value only works with interface projections, not DTO records

What is the performance difference?

For a table with 20 columns and 10,000 rows where the API needs only 3 columns, projections dramatically reduce resource usage.

JAVA
package com.bank.service;

import com.bank.dto.TradeIdAndSymbol;
import com.bank.dto.TradeOverview;
import com.bank.model.Trade;
import com.bank.repository.TradeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class PerformanceComparisonService {

    private final TradeRepository tradeRepo;

    public PerformanceComparisonService(TradeRepository tradeRepo) {
        this.tradeRepo = tradeRepo;
    }

    @Transactional(readOnly = true)
    public void compareProjections() {
        // FULL ENTITY: SELECT * (all 20 columns) + persistence context
        List<Trade> full = tradeRepo.findByStatus("SETTLED");
        // Memory: ~10MB (data + snapshots)

        // INTERFACE: SELECT trade_id, symbol, amount (3 columns) + proxy
        List<TradeIdAndSymbol> iface =
            tradeRepo.findByStatus("SETTLED");
        // Memory: ~500KB (proxy objects)

        // DTO: SELECT trade_id, symbol, amount (3 columns) + plain record
        List<TradeOverview> dto =
            tradeRepo.findOverviewByStatus("SETTLED");
        // Memory: ~300KB (plain records, no proxy)
    }
}
TEXT
Performance comparison (10,000 rows, 20-column table):

  Full entity:
    SQL: SELECT * FROM trades (20 columns x 10K = 200K fields)
    Memory: ~5MB data + ~5MB snapshots = 10MB
    Dirty checking: 20,000 comparisons at flush

  Interface projection:
    SQL: SELECT trade_id, symbol, amount (3 columns x 10K = 30K fields)
    Memory: ~500KB (proxy objects)
    Dirty checking: none

  DTO projection:
    SQL: SELECT trade_id, symbol, amount (3 columns x 10K = 30K fields)
    Memory: ~300KB (plain records)
    Dirty checking: none
    Winner: 85% less data, 97% less memory

What are common mistakes with projections?

The most common mistake is using full entities for read-only API responses. The second mistake is forgetting the fully qualified class name in constructor expressions.

JAVA
package com.bank.repository;

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

public interface TradeRepository extends JpaRepository<Trade, String> {

    // WRONG: no package name in constructor expression
    // @Query("SELECT new TradeOverview(t.tradeId, t.symbol, t.amount) FROM Trade t")
    // Throws: org.hibernate.hql.internal.ast.QuerySyntaxException

    // CORRECT: fully qualified class name
    @Query("SELECT new com.bank.dto.TradeOverview(t.tradeId, t.symbol, t.amount) "
         + "FROM Trade t WHERE t.status = 'SETTLED'")
    List<TradeOverview> findSettledOverviews();
}

When should you use each projection type?

Use DTO projections for production API responses – they are the fastest and produce the lightest payloads. Use interface projections for quick prototyping or when you need SpEL computed fields. Use full entities only when you need to modify the data.

JAVA
package com.bank.service;

import com.bank.dto.TradeOverview;
import com.bank.model.Trade;
import com.bank.repository.TradeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class TradeDecisionService {

    private final TradeRepository tradeRepo;

    public TradeDecisionService(TradeRepository tradeRepo) {
        this.tradeRepo = tradeRepo;
    }

    // READ: use DTO projection (fastest, lightest)
    @Transactional(readOnly = true)
    public java.util.List<TradeOverview> listTrades(String status) {
        return tradeRepo.findOverviewByStatus(status);
    }

    // WRITE: use full entity (dirty checking needed)
    @Transactional
    public void settleTrade(String tradeId) {
        Trade trade = tradeRepo.findById(tradeId).orElseThrow();
        trade.setStatus("SETTLED");
        // Dirty checking detects the change and generates UPDATE
    }
}

Visual Overview

DIAGRAM
  Full Entity:
  ┌───────────────────────────────────┐
  │  SELECT * FROM trades (20 columns)  │
  │  → Entity in persistence context    │
  │  → Snapshot stored for dirty check  │
  │  → 10MB memory for 10K rows         │
  └───────────────────────────────────┘

  Interface Projection:
  ┌───────────────────────────────────┐
  │  SELECT id, symbol, amount (3 cols) │
  │  → Spring-generated proxy object    │
  │  → NOT in persistence context       │
  │  → 500KB memory for 10K rows        │
  └───────────────────────────────────┘

  DTO Projection (fastest):
  ┌───────────────────────────────────┐
  │  SELECT id, symbol, amount (3 cols) │
  │  → Plain record/object (no proxy)   │
  │  → NOT in persistence context       │
  │  → 300KB memory for 10K rows        │
  └───────────────────────────────────┘

Key Takeaway

JPA projections fetch only the columns your API needs, dramatically reducing data transfer, memory, and CPU. Interface projections declare getter methods in an interface – Spring generates a proxy. They uniquely support SpEL @Value for computed fields like getDisplayName(). DTO projections use a JPQL constructor expression or a record – no proxy, direct object creation, and the fastest approach.

At National Bank of India, the Trade entity has 12 columns including large TEXT fields for notes and audit history. The trade listing API needs only tradeId, symbol, and amount. Rajesh uses DTO projection TradeOverview(tradeId, symbol, amount) which fetches 3 columns instead of 12 – 85% less data transferred. For 10,000 trades: full entity loads 120,000 field values plus 10,000 snapshots. DTO loads 30,000 field values with zero snapshots.

Always prefer DTO projections for read-only API endpoints. Use interface projections for quick prototyping or when you need SpEL computed fields. Use full entities only when you need to modify the data, because dirty checking requires the entity in the persistence context.

Remember one sentence: DTO projections fetch only what you need, skip the persistence context entirely, and are the fastest way to read data through JPA.

---

Share
Use ← → arrow keys to navigate