Spring Data JPA
Projections
Specifications
Java
Data Access

How to use projections and specifications with spring data jpa?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Spring Data JPA Specification and projection support solve two different problems that often appear in the same query. A Specification builds dynamic filtering logic, while a projection reduces the selected data to the fields a caller actually needs. When you combine them well, you get flexible search screens without paying the cost of loading full entities for every result row.

Start with an Entity and a Specification-Capable Repository

The repository must extend JpaSpecificationExecutor if you want dynamic criteria support.

java
1import jakarta.persistence.*;
2import java.math.BigDecimal;
3import java.time.Instant;
4
5@Entity
6@Table(name = "orders")
7public class OrderEntity {
8
9    @Id
10    @GeneratedValue(strategy = GenerationType.IDENTITY)
11    private Long id;
12
13    private String customerName;
14    private String status;
15    private BigDecimal totalAmount;
16    private Instant createdAt;
17
18    public Long getId() { return id; }
19    public String getCustomerName() { return customerName; }
20    public String getStatus() { return status; }
21    public BigDecimal getTotalAmount() { return totalAmount; }
22    public Instant getCreatedAt() { return createdAt; }
23}
java
1import org.springframework.data.jpa.repository.JpaRepository;
2import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
3
4public interface OrderRepository extends
5        JpaRepository<OrderEntity, Long>,
6        JpaSpecificationExecutor<OrderEntity> {
7}

That gives you a clean base to build on.

Build Small, Composable Specifications

The mistake most teams make is building one huge query method that handles every optional filter. Split filters into small specifications and compose them.

java
1import org.springframework.data.jpa.domain.Specification;
2
3import java.math.BigDecimal;
4import java.time.Instant;
5
6public final class OrderSpecifications {
7
8    private OrderSpecifications() {
9    }
10
11    public static Specification<OrderEntity> hasStatus(String status) {
12        return (root, query, cb) ->
13                status == null || status.isBlank()
14                        ? cb.conjunction()
15                        : cb.equal(root.get("status"), status);
16    }
17
18    public static Specification<OrderEntity> minTotal(BigDecimal minTotal) {
19        return (root, query, cb) ->
20                minTotal == null
21                        ? cb.conjunction()
22                        : cb.greaterThanOrEqualTo(root.get("totalAmount"), minTotal);
23    }
24
25    public static Specification<OrderEntity> createdAfter(Instant from) {
26        return (root, query, cb) ->
27                from == null
28                        ? cb.conjunction()
29                        : cb.greaterThanOrEqualTo(root.get("createdAt"), from);
30    }
31}

Returning cb.conjunction() for a missing filter keeps composition easy and avoids a lot of null branching.

Use Projections for Read-Oriented Results

If a screen only needs three columns, returning full entities is wasteful. An interface projection is the lightest option for simple read models:

java
1import java.math.BigDecimal;
2
3public interface OrderSummaryView {
4    Long getId();
5    String getCustomerName();
6    BigDecimal getTotalAmount();
7}

This is useful for list views, exports, and autocomplete-style responses where you do not want the full entity graph.

For more custom shaping, use a DTO or Java record:

java
1import java.math.BigDecimal;
2
3public record OrderSummaryDto(Long id, String customerName, BigDecimal totalAmount) {
4}

Use interface projections when field mapping is direct. Use DTOs when the API contract should be independent from entity property names.

Combine Projections and Specifications with the Fluent Query API

The cleanest approach in recent Spring Data JPA versions is the findBy fluent query method on JpaSpecificationExecutor.

java
1import org.springframework.data.domain.Sort;
2import org.springframework.data.jpa.domain.Specification;
3import org.springframework.stereotype.Service;
4
5import java.math.BigDecimal;
6import java.time.Instant;
7import java.util.List;
8
9@Service
10public class OrderQueryService {
11
12    private final OrderRepository orderRepository;
13
14    public OrderQueryService(OrderRepository orderRepository) {
15        this.orderRepository = orderRepository;
16    }
17
18    public List<OrderSummaryView> search(String status, BigDecimal minTotal, Instant createdAfter) {
19        Specification<OrderEntity> spec = Specification
20                .where(OrderSpecifications.hasStatus(status))
21                .and(OrderSpecifications.minTotal(minTotal))
22                .and(OrderSpecifications.createdAfter(createdAfter));
23
24        return orderRepository.findBy(spec, query -> query
25                .as(OrderSummaryView.class)
26                .sortBy(Sort.by(Sort.Direction.DESC, "createdAt"))
27                .all());
28    }
29}

That gives you dynamic filters and a reduced read model in one repository call.

Add Paging and Performance Discipline

Most queries that use specifications eventually need paging. Without it, even a well-projected query can still return too much data.

java
1import org.springframework.data.domain.Page;
2import org.springframework.data.domain.Pageable;
3import org.springframework.data.jpa.domain.Specification;
4
5public Page<OrderSummaryView> searchPage(Specification<OrderEntity> spec, Pageable pageable) {
6    return orderRepository.findBy(spec, query -> query
7            .as(OrderSummaryView.class)
8            .page(pageable));
9}

Projection is not a substitute for indexing. If the filters behind your Specification use status, createdAt, or totalAmount frequently, index those columns in the database. Also watch for hidden joins caused by lazy associations referenced inside projections or serializers.

In practice, you should validate the generated SQL in integration tests or local logs. The code may look efficient while the query planner tells a different story.

Common Pitfalls

  • Returning full entities for list endpoints when only a few columns are needed.
  • Building one giant specification instead of composing smaller reusable predicates.
  • Assuming every repository method supports projection and specification combinations the same way.
  • Skipping database indexes on fields used heavily by dynamic filters.
  • Leaking entity structure directly into external API contracts when a DTO would be more stable.

Summary

  • 'Specification is for dynamic filtering and projection is for lean result shapes.'
  • Keep specifications small and composable so optional filters stay readable.
  • Use interface projections for simple read models and DTOs for explicit API contracts.
  • The fluent findBy API is the cleanest way to combine both features.
  • Add paging, indexing, and SQL verification so flexible queries remain fast in production.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.