Skip to content

Spring Boot · Data Access

Spring Data JPA Repositories, Explained

6 min read Updated 2026-06-26 Share:

Practice Spring Data Repositories interview questions

DAOs you declare instead of write

The defining trick of Spring Data is that you declare an interface and Spring supplies the implementation at runtime. There's no DAO class, no boilerplate EntityManager plumbing, no hand-written CRUD. This article covers how that works, the four ways to express a query, and the runtime machinery underneath — the parts interviewers actually probe.

Declaring a repository

interface CustomerRepository extends JpaRepository<Customer, Long> {
    List<Customer> findByLastName(String lastName);
}

Parameterize with the entity and its id type. You inherit save, findById, findAll, delete, count, existsById and more for free, and Spring generates a proxy bean you can inject anywhere. You write only declarations.

The hierarchy

The interfaces stack, each adding capability:

Repository (marker)
  └─ CrudRepository           save, findById, findAll, delete, count
       └─ PagingAndSortingRepository  findAll(Pageable / Sort)
            └─ JpaRepository    flush, saveAndFlush, deleteAllInBatch, getReferenceById

Extend JpaRepository by default for the full toolkit; drop to CrudRepository only when you want to deliberately limit the exposed surface.

Four ways to express a query

1. Derived query methods — Spring parses the method name into JPQL at startup:

List<User> findByEmailAndActiveTrue(String email);
Optional<User> findFirstByOrderByCreatedAtDesc();
boolean existsByEmail(String email);

Keywords like And, Between, GreaterThan, Like, In, OrderBy, and the find/count/exists/ delete prefixes cover a lot. If a name can't be parsed, the app fails to start — a useful early check. But names get unwieldy fast.

2. @Query (JPQL) — when the name would be awkward:

@Query("SELECT u FROM User u WHERE u.lastName LIKE %:term%")
List<User> search(@Param("term") String term);

JPQL queries entity and field names, so it's validated against your mapping and portable across databases. Bind by name with @Param.

3. Native SQL — for vendor-specific features JPQL can't express:

@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'", nativeQuery = true)
List<User> recentlyJoined();

Native queries use table and column names, bypass JPQL validation, and tie you to one dialect — use them sparingly.

4. Modifying queries — bulk updates and deletes need @Modifying:

@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :cutoff")
int deactivateStale(@Param("cutoff") LocalDateTime cutoff);

@Modifying tells Spring to call executeUpdate(). Two gotchas: it must run in a transaction, and it bypasses the persistence context, so entities already in memory keep stale values — add clearAutomatically = true or re-fetch.

Paging, sorting, and what to return

Pass a Pageable and return a Page; Spring adds LIMIT/OFFSET and runs a count query:

Page<User> findByActiveTrue(Pageable pageable);

Pageable p = PageRequest.of(0, 20, Sort.by("lastName").ascending());
Page<User> page = repo.findByActiveTrue(p);
page.getTotalElements();   // total matching rows

Return Page when you need the total, Slice for infinite scroll where you only need "is there a next page?" (no count query, so it's cheaper), or a plain List to skip counting. For single results, return Optional — it makes "not found" explicit and dodges NPEs. Use count/exists instead of fetching rows just to check existence.

Projections for read-only views

When you only need a few columns, project instead of loading the whole entity:

interface UserView {                 // closed interface projection
    String getEmail();
    String getLastName();
}
List<UserView> findByActiveTrue();   // SELECT email, last_name ...

A closed projection (only entity properties) lets Spring narrow the SELECT to exactly those columns — ideal for list and summary screens, with no lazy-loading surprises. A DTO/record projection works the same way. Open projections with SpEL load the full entity first, so prefer closed ones for the optimization.

How the implementation is generated

There's no code on disk. At startup Spring scans for repository interfaces and builds a JDK dynamic proxy for each:

repo.findByEmail(x)
  → dynamic proxy
  → QueryExecutorMethodInterceptor
  → custom impl? derived query? @Query? named query?
  → runs it via SimpleJpaRepository

Base CRUD comes from SimpleJpaRepository; derived methods are parsed by a PartTree; @Query uses the declared string. This runtime-proxy design is exactly why a typo in a derived method name shows up as a startup failure, not a compile error.

Escape hatches: custom implementations and getReferenceById

When derived methods and static @Query strings aren't enough — dynamic queries via the Criteria API or QueryDSL — add a fragment and an Impl:

interface UserRepositoryCustom { List<User> searchComplex(SearchCriteria c); }
class UserRepositoryCustomImpl implements UserRepositoryCustom {   // "Impl" suffix is the convention
    @PersistenceContext EntityManager em;
    public List<User> searchComplex(SearchCriteria c) { /* dynamic JPQL / Criteria */ }
}
interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {}

And a small but useful optimization: getReferenceById returns a lazy proxy without a SELECT when you only need to set a foreign key:

order.setCustomer(customerRepo.getReferenceById(customerId));  // no query — just sets the FK

The trade-off is that a missing id surfaces later as EntityNotFoundException, not immediately. Use findById when you actually need the data or must verify existence now.

Finding the repositories

@SpringBootApplication implies @EnableJpaRepositories for the main class's package and below, so repositories there are picked up automatically. The classic "no repository bean found" error almost always means the interface lives outside that package tree — add an explicit @EnableJpaRepositories(basePackages = "...") for repositories elsewhere or for multiple datasources.

Recap

Declare an interface extending JpaRepository; choose the lightest query mechanism that's still readable — derived methods, then @Query, then native SQL, with @Modifying for bulk writes; return Optional, Page, or Slice to match the use case; project into closed interfaces or DTOs for read-only views; and remember it's all a runtime JDK proxy over SimpleJpaRepository, which explains the startup-time validation. Add a ...Custom/...Impl fragment when you need dynamic queries. You write intent; Spring writes the DAO.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel