Lesson 15 / 25

DTO Projections and Read Models

Return exactly the data a use case needs with constructor, record and interface projections.

Not every read needs entities

Entities are ideal when you will change data: they are tracked, dirty-checked and cascade. For read-only use cases such as lists, reports and API responses, loading full entities wastes memory and invites lazy-loading problems. DTO projections select only the needed columns directly into data objects. In JPQL, a constructor expression creates DTOs: select new com.shop.OrderSummary(o.id, c.name, o.total) from Order o join o.customer c. Java records are perfect DTOs. Spring Data JPA adds interface projections (declare getters for the needed properties and Spring creates proxies) and class-based projections (records or classes). Projections are not managed, so there is no dirty checking and no snapshot overhead, and they produce focused SQL. For complex reporting, native SQL or a dedicated query tool is often clearer than forcing the ORM. A useful rule of thumb: entities for writes, projections for reads.

Record projections with JPQL and Spring Data

Only three columns are selected; nothing is tracked by the persistence context.

public record OrderSummary(Long id, String customerName, BigDecimal total, OrderStatus status) { }

List<OrderSummary> summaries = em.createQuery("""
        select new com.shop.orders.OrderSummary(o.id, c.name, o.total, o.status)
        from PurchaseOrder o join o.customer c
        where o.status = :status
        order by o.placedAt desc
        """, OrderSummary.class)
    .setParameter("status", OrderStatus.PAID)
    .setMaxResults(50)
    .getResultList();

public interface OrderRepository extends JpaRepository<PurchaseOrder, Long> {
    // class-based projection with a JPQL constructor expression
    @Query("""
        select new com.shop.orders.OrderSummary(o.id, c.name, o.total, o.status)
        from PurchaseOrder o join o.customer c where c.id = :customerId
        """)
    List<OrderSummary> summariesForCustomer(Long customerId);

    // interface projection: Spring generates the query and the implementation
    interface OrderIdAndTotal { Long getId(); BigDecimal getTotal(); }
    List<OrderIdAndTotal> findByStatus(OrderStatus status);
}

Never return entities from REST controllers

Serialising entities exposes internal fields, triggers lazy loading during JSON rendering and couples your API to your schema. Map to DTOs inside the transaction.

Quick check: Why are DTO projections often better than entities for read-only list screens?

  • They are required by JPA
  • They automatically cache results forever
  • They select only needed columns and avoid persistence-context tracking and lazy-loading issues
  • They can update the database directly
Answer

They select only needed columns and avoid persistence-context tracking and lazy-loading issues — Projections fetch exactly what the screen needs without entity management overhead.