Back to Projections: Interface, DTO, Dynamic
Q2Question 2 of 4

Interface vs DTO vs dynamic projections?

Key Concepts
  • Interface projection supports SpEL @Value for computed fields like getDisplayName() – a feature DTO projections cannot offer
  • DTO projection with constructor expression is the fastest: no proxy, no persistence context, direct object creation
  • Nested interface projections let you project associations (account.getTrades() returns List<TradeView>)
  • JPQL constructor expressions do not support nested collections – use interface projections for nested data
  • Dynamic projections accept Class<T> so the same repository method can return different shapes at runtime
  • For a 20-column entity where the API needs 3 columns, projections reduce data transfer by 85%
  • Use DTO for production APIs, interface for computed fields and nested projections, full entity only for writes

Definition

Interface projections support SpEL @Value for computed fields and nested interfaces for associations; DTO projections are the fastest with no proxy overhead; dynamic projections let the caller choose the shape at runtime.
  • Interface with SpEL – interface projections support @Value("#{target.field + ...}") for computed fields. The SpEL expression evaluates against the target entity at runtime.
  • Nested interface projection – interfaces can declare nested interfaces for association projections: List getTrades(). Spring handles the JOIN and mapping.
  • DTO constructor expression – SELECT new Dto(e.f1, e.f2) creates plain objects directly. No proxy, no persistence context. Does not support nested collections.
  • Dynamic projection – List findBy(String x, Class type) lets the caller pick the return shape. Same method, different projections.
  • Performance – DTO is fastest (no proxy, no persistence context). Interface adds proxy overhead but enables SpEL and nesting. Full entity is slowest (all columns, snapshots, dirty checking).

At National Bank of India, the full Account entity is like Rajesh's entire 20-page customer file. Interface projection with SpEL is like a pre-printed slip that computes "Rajesh Kumar (SAV2001)" on the fly. Nested interface projection is like a slip that also lists the last 5 trades. DTO projection is like a receipt printed directly from the database with exactly the fields the mobile app needs. Dynamic projection is like Rajesh choosing which slip format he wants at the counter.

Understanding the Concepts

How does interface projection support SpEL computed fields?

Interface projections support @Value with SpEL expressions to compute fields at runtime. This is something DTO projections cannot do.

JAVA
package com.bank.dto;

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

public interface AccountSummary {
    String getAccountId();
    String getHolderName();
    BigDecimal getBalance();

    @Value("#{target.holderName + ' (' + target.accountId + ')'}")
    String getDisplayName();
    // Computed at runtime: "Rajesh Kumar (SAV2001)"
    // SpEL evaluates against the target entity
    // SQL only fetches: account_id, holder_name, balance
}
JAVA
package com.bank.repository;

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

public interface AccountRepository extends JpaRepository<Account, String> {

    List<AccountSummary> findByHolderNameContaining(String name);
    // SQL: SELECT account_id, holder_name, balance
    //      FROM accounts WHERE holder_name LIKE '%name%'
    // getDisplayName() computed by SpEL at runtime
}

How do nested interface projections work?

Interfaces can declare nested interfaces for association projections. Spring handles the JOIN and mapping automatically.

JAVA
package com.bank.dto;

import java.math.BigDecimal;
import java.util.List;

public interface AccountWithTradesView {
    String getAccountId();
    String getHolderName();
    BigDecimal getBalance();
    List<TradeView> getTrades();  // nested projection!

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

import com.bank.dto.AccountWithTradesView;
import com.bank.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface AccountRepository extends JpaRepository<Account, String> {

    Optional<AccountWithTradesView> findByAccountId(String accountId);
    // Returns account with nested trade projections
    // Only selected fields fetched from both tables
}

Why can DTO constructor expressions not handle nested collections?

JPQL constructor expressions create objects with scalar values. They cannot map a collection (like List) into a constructor parameter. For nested data, use interface projections or build the DTO manually in the service layer.

JAVA
package com.bank.dto;

import java.math.BigDecimal;

// DTO record: works for flat data
public record TradeOverview(String tradeId, String symbol, BigDecimal amount) {}

// This DOES NOT work as a constructor expression:
// public record AccountWithTrades(
//     String accountId,
//     String holderName,
//     List<TradeOverview> trades  // cannot map collection in JPQL constructor!
// ) {}
JAVA
package com.bank.service;

import com.bank.dto.TradeOverview;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class AccountDetailService {

    private final AccountRepository accountRepo;

    public AccountDetailService(AccountRepository accountRepo) {
        this.accountRepo = accountRepo;
    }

    // Build nested DTO manually in service layer
    @Transactional(readOnly = true)
    public AccountDetailDTO getDetail(String accountId) {
        Account account = accountRepo.findWithTrades(accountId).orElseThrow();
        List<TradeOverview> trades = account.getTrades().stream()
            .map(t -> new TradeOverview(t.getTradeId(), t.getSymbol(), t.getAmount()))
            .toList();
        return new AccountDetailDTO(
            account.getAccountId(),
            account.getHolderName(),
            account.getBalance(),
            trades
        );
    }
}

How does dynamic projection let the caller choose?

Dynamic projection uses a Class parameter so the same repository method returns different projections based on what the caller requests.

JAVA
package com.bank.repository;

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

public interface AccountRepository extends JpaRepository<Account, String> {

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

import com.bank.dto.AccountSummary;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class AccountSearchService {

    private final AccountRepository accountRepo;

    public AccountSearchService(AccountRepository accountRepo) {
        this.accountRepo = accountRepo;
    }

    @Transactional(readOnly = true)
    public void dynamicDemo() {
        // Same method, different projections:
        List<AccountSummary> summaries =
            accountRepo.findByHolderNameContaining("Rajesh", AccountSummary.class);
        // Only 3 columns + computed displayName

        List<Account> fullEntities =
            accountRepo.findByHolderNameContaining("Rajesh", Account.class);
        // All columns, full entity in persistence context
    }
}

Why do projections beat full entity loading?

Projections reduce three costs: network (fewer columns transferred), memory (no persistence context), and CPU (no dirty checking).

TEXT
20-column entity, 10,000 rows, API needs 3 columns:

  Full entity:
    Data: 20 cols x 10K = 200K field reads
    Memory: ~5MB data + ~5MB snapshots = 10MB
    CPU: 200K dirty checking comparisons at flush
    Network: 500 bytes/row x 10K = 5MB from DB

  DTO projection (3 columns):
    Data: 3 cols x 10K = 30K field reads
    Memory: ~300KB (plain records)
    CPU: zero dirty checking
    Network: 75 bytes/row x 10K = 750KB from DB

  Savings: 85% less data, 97% less memory, zero dirty checking

How is interface projection different from DTO projection?

| Feature | Interface | DTO (Record/Class) |
|---------|----------|--------------------|
| SpEL @Value | Supported | Not supported |
| Nested projections | Supported | Not in constructor expression |
| Implementation | Spring proxy | Direct instantiation |
| Performance | Proxy overhead | No overhead (fastest) |
| Definition | Interface with getters | Record with constructor |
| Use case | Computed fields, nesting | Production API responses |

JAVA
package com.bank.dto;

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

// Interface: supports SpEL and nesting
public interface AccountSummary {
    String getAccountId();
    String getHolderName();
    @Value("#{target.holderName + ' (' + target.accountId + ')'}")
    String getDisplayName();  // computed!
}

// DTO record: fastest, no proxy
// public record AccountOverview(String accountId, String holderName) {}
// No SpEL support, no nesting

What are common mistakes with projections?

The most common mistake is returning full entities from read-only API endpoints when a projection would be more efficient.

JAVA
package com.bank.controller;

import com.bank.dto.TradeOverview;
import com.bank.model.Trade;
import com.bank.repository.TradeRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
public class TradeController {

    private final TradeRepository tradeRepo;

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

    // WRONG: returning full entity for a list API
    // @GetMapping("/trades")
    // public List<Trade> getTrades() {
    //     return tradeRepo.findByStatus("SETTLED");
    //     // All 20 columns loaded, 10MB for 10K rows
    // }

    // CORRECT: return DTO projection
    @GetMapping("/trades")
    public List<TradeOverview> getTrades() {
        return tradeRepo.findOverviewByStatus("SETTLED");
        // Only 3 columns loaded, 300KB for 10K rows
    }
}

When should you use each projection form?

Use DTO projections for production API responses (fastest). Use interface projections when you need SpEL computed fields or nested association projections. Use dynamic projections when the same method needs to return different shapes. Use full entities only when you need to modify the data.

JAVA
package com.bank.service;

import com.bank.dto.AccountSummary;
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 ProjectionDecisionService {

    private final TradeRepository tradeRepo;

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

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

    // WRITE: full entity (dirty checking needed)
    @Transactional
    public void settleTrade(String tradeId) {
        Trade trade = tradeRepo.findById(tradeId).orElseThrow();
        trade.setStatus("SETTLED");
    }
}

Visual Overview

DIAGRAM
  Projection Decision Tree:
  ┌───────────────────────────┐
  │  Need to WRITE data?       │
  └───────┬───────┬───────────┘
        YES           NO
         │             │
         ▼             ▼
  ┌──────────┐  ┌─────────────────┐
  │Full Entity│  │ Need SpEL or     │
  │(dirty chk)│  │ nested assoc?    │
  └──────────┘  └───┬──────┬──────┘
                  YES       NO
                   │         │
                   ▼         ▼
            ┌─────────┐ ┌──────────┐
            │Interface│ │DTO Record│
            │(proxy)  │ │(fastest)  │
            └─────────┘ └──────────┘

Key Takeaway

Interface projections support two features that DTO projections cannot: SpEL @Value for computed fields and nested interface projections for associations. The SpEL expression #{target.holderName + ' (' + target.accountId + ')'} computes "Rajesh Kumar (SAV2001)" at runtime without an extra database column. Nested interfaces project both the parent and its collection in one query.

DTO projections are the fastest approach: no proxy, no persistence context, direct object creation. At National Bank of India, Rajesh uses DTO record TradeOverview(tradeId, symbol, amount) for the trade listing API – 3 columns instead of 20, 85% less data, 97% less memory. For the account search dropdown, he uses interface projection with SpEL to compute the display name.

Dynamic projections add runtime flexibility: List findByName(String name, Class type) lets the mobile API request AccountSummary.class (3 fields) while the admin dashboard requests Account.class (all fields), both using the same repository method. Always use projections for read-only endpoints. Use full entities only when dirty checking is needed for writes.

Remember one sentence: use DTO projections for production APIs, interface projections for computed fields and nested data, and full entities only for writes.

---

Share
Use ← → arrow keys to navigate