Spring Data Repositories Interview Questions & Answers
Spring Data JPA repositories — the repository hierarchy, derived query methods, @Query JPQL and native queries, paging and sorting, projections, modifying queries, and how Spring generates the implementation at runtime.
A Spring Data repository is an interface you declare — Spring generates the implementation at runtime, so you write no DAO code for standard CRUD and query operations.
interface CustomerRepository extends JpaRepository<Customer, Long> {
// inherited for free: save, findById, findAll, delete, count, existsById, ...
List<Customer> findByLastName(String lastName); // implemented from the method name
}
You parameterize it with the entity type and its id type. Spring scans for interfaces extending a repository marker, creates a proxy bean for each, and wires it wherever you inject it. You only ever write declarations; the framework provides the behavior.
Rule of thumb: Declare a repository interface extending JpaRepository<Entity, Id> and let
Spring generate the implementation — you never write the CRUD boilerplate.
The interfaces build on each other, each adding capability:
Repository (marker, no methods)
└─ CrudRepository<T, ID> save, findById, findAll, delete, count
└─ PagingAndSortingRepository findAll(Pageable), findAll(Sort)
└─ JpaRepository<T, ID> flush, saveAndFlush, deleteAllInBatch, getReferenceById
Repository— empty marker; the root of the hierarchy.CrudRepository— basic CRUD.PagingAndSortingRepository— adds paging and sorting.JpaRepository— adds JPA-specific batch ops and flushing.
Most projects extend JpaRepository for the full set. Extend a narrower interface only if you
deliberately want to expose a smaller surface.
Rule of thumb: Extend JpaRepository by default; drop to CrudRepository only when you want
to intentionally limit the available operations.
Spring Data parses the method name into a JPQL query at startup — keywords map to WHERE conditions, so the name is the query.
List<User> findByEmailAndActiveTrue(String email);
List<User> findByAgeGreaterThanOrderByAgeDesc(int age);
Optional<User> findFirstByOrderByCreatedAtDesc();
long countByActiveFalse();
boolean existsByEmail(String email);
Supported keywords include And, Or, Between, LessThan, GreaterThan, Like,
Containing, In, IsNull, True/False, OrderBy, and prefixes find/read/count/
exists/delete. If a name can't be parsed, the app fails at startup — a useful early
check. Names get unwieldy fast, though.
Rule of thumb: Use derived queries for simple, readable lookups; once a method name gets long
or awkward, switch to @Query.
Use @Query to write JPQL (or SQL) explicitly when a derived name would be unwieldy or you
need joins, projections, or expressions the name parser can't express.
@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
Optional<User> findActiveByEmail(@Param("email") String email);
@Query("SELECT u FROM User u WHERE u.lastName LIKE %:term%")
List<User> search(@Param("term") String term);
Bind parameters by name (:email with @Param) — clearer than positional ?1. JPQL queries
against entity/field names, not table/column names, so they're validated against your mapping
and stay portable across databases. Spring even validates the JPQL at startup.
Rule of thumb: Reach for @Query with named parameters when method-name derivation gets
awkward; it keeps complex queries readable and database-portable.
Set nativeQuery = true on @Query to run raw SQL against the actual tables — for
database-specific features JPQL can't express (window functions, vendor functions, hints).
@Query(value = "SELECT * FROM users u WHERE u.created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<User> recentlyJoined();
Native queries use table and column names, not entity/field names, so they bypass JPQL
validation and tie you to a specific database dialect. They also don't support JPQL paging niceties
the same way (you supply a countQuery for Page). Use them sparingly, only when JPQL genuinely
can't do the job.
Rule of thumb: Prefer JPQL for portability; use nativeQuery = true only for vendor-specific
SQL JPQL can't express, accepting the loss of portability.
A @Query is a SELECT by default. @Modifying marks it as an UPDATE/DELETE/DDL
statement, telling Spring to call executeUpdate() instead of running a select.
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :cutoff")
int deactivateStale(@Param("cutoff") LocalDateTime cutoff); // returns affected row count
Two gotchas: it must run inside a transaction (@Transactional), and a bulk update bypasses
the persistence context — entities already loaded in memory keep their stale values. Add
clearAutomatically = true (or re-fetch) so you don't read pre-update state afterward.
Rule of thumb: Annotate bulk UPDATE/DELETE queries with @Modifying inside a transaction,
and clear the context since they skip dirty checking.
Pass a Pageable (or Sort) argument and return a Page. Spring adds the LIMIT/OFFSET
and runs a separate count query so you know the total.
Page<User> findByActiveTrue(Pageable pageable);
// caller:
Pageable p = PageRequest.of(0, 20, Sort.by("lastName").ascending());
Page<User> page = repo.findByActiveTrue(p);
page.getTotalElements(); // total matching rows
page.getContent(); // this page's 20 rows
Return Page when you need the total count, Slice when you only need "is there a next
page?" (cheaper — no count query), or a plain List/Pageable to skip counting entirely. The
same Pageable works on derived and @Query methods alike.
Rule of thumb: Return Page when you need totals, Slice for infinite-scroll where the count
isn't needed — and let Pageable carry both paging and sorting.
Spring Data supports many wrapper and collection types and adapts the query accordingly:
Optional<User> findByEmail(String email); // 0 or 1 — preferred for single results
User findById(Long id); // may return null
List<User> findByActiveTrue(); // a collection
Stream<User> streamByActiveTrue(); // lazy stream (close it!)
Page<User> findByLastName(String n, Pageable p);
long countByActiveTrue(); // aggregate
boolean existsByEmail(String email);
Prefer Optional for single results — it makes "not found" explicit and avoids NPEs. A single
query that returns multiple rows throws IncorrectResultSizeDataAccessException. Stream must be
used inside a transaction and closed (try-with-resources).
Rule of thumb: Return Optional for at-most-one results, collections for many, and use
count/exists instead of fetching rows just to check.
Projections let a query return a subset of an entity's data instead of the full entity — Spring selects only the columns the projection exposes.
interface UserView { // closed interface projection
String getEmail();
String getLastName();
}
List<UserView> findByActiveTrue(); // SELECT email, last_name ...
record UserDto(String email, String lastName) {} // class/DTO projection
List<UserDto> findByActiveTrueAndLastName(String n);
A closed projection (only entity properties) lets Spring optimize the SELECT to just those
columns. An open projection with @Value("#{...}") SpEL pulls the whole entity first.
Projections are ideal for read-only list/summary views — less data, no lazy-loading traps.
Rule of thumb: Use closed interface or DTO projections for read-only views so the query fetches only the columns you actually render.
At startup, Spring Data scans for repository interfaces and creates a dynamic proxy for each. Method calls route through a chain that decides how to satisfy them.
yourRepo.findByEmail(x)
→ JDK dynamic proxy
→ QueryExecutorMethodInterceptor
→ is it a custom impl? derived query? @Query? named query?
→ builds & runs the JPA query via SimpleJpaRepository
The base CRUD behavior comes from SimpleJpaRepository; derived methods are parsed into
queries by a PartTree; @Query methods use the declared string. There's no code generation on
disk — it's runtime proxying. That's why a typo in a derived method name surfaces as a
startup failure, not a compile error.
Rule of thumb: Repositories are runtime JDK proxies backed by SimpleJpaRepository — knowing
this explains why query-method errors appear at startup, not compile time.
Define a fragment interface plus an Impl class, then have your repository extend both. Spring
wires the custom implementation alongside the generated one.
interface UserRepositoryCustom { // 1. the custom contract
List<User> searchComplex(SearchCriteria c);
}
class UserRepositoryCustomImpl implements UserRepositoryCustom { // 2. "...Impl" by convention
@PersistenceContext EntityManager em;
public List<User> searchComplex(SearchCriteria c) {
// hand-built Criteria API / dynamic JPQL here
}
}
interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {} // 3.
The Impl suffix is the naming convention that links the fragment to its implementation. This
is the escape hatch for dynamic queries (Criteria API, QueryDSL) that don't fit derived methods
or static @Query strings.
Rule of thumb: Add a ...Custom fragment + ...Impl class for dynamic or complex queries, and
extend both interfaces from your repository.
All persist, but they differ in when SQL hits the database and how save decides
insert-vs-update.
repo.save(entity); // schedules INSERT/UPDATE; SQL flushes at transaction commit
repo.saveAndFlush(entity); // forces the SQL to the DB immediately (e.g. to read it back)
repo.saveAll(list); // saves a collection, still batched at flush
save decides between insert and update by whether the entity is new (null/unset id → INSERT,
otherwise it merges). saveAndFlush is for when you must see the row mid-transaction (a
generated id, a DB trigger, validating a constraint early). Most code should just use save and
let flush happen at commit.
Rule of thumb: Use save and let Hibernate flush at commit; reach for saveAndFlush only when
you must force the SQL out mid-transaction.
Query by Example (QBE) builds a query from a probe — a populated entity instance whose non-null fields become the WHERE conditions — no JPQL or method name required.
User probe = new User();
probe.setActive(true);
probe.setLastName("Smith");
Example<User> example = Example.of(probe); // matches active users named Smith
List<User> matches = repo.findAll(example); // via JpaRepository (extends QueryByExampleExecutor)
Use an ExampleMatcher to tune matching (ignore case, "contains" instead of "equals", ignore
specific paths). It's handy for dynamic search forms where the set of filters varies. Its
limits: no OR, no ranges, no nested property traversal — for those, use Criteria/QueryDSL.
Rule of thumb: Use Query by Example for simple, dynamic equality-based filters; graduate to the Criteria API or QueryDSL when you need ranges, OR, or nested conditions.
Spring Boot auto-configures repository scanning. @SpringBootApplication triggers
@EnableJpaRepositories for the package of the main class and its sub-packages, so any
repository interface there is detected automatically.
@SpringBootApplication // implies @EnableJpaRepositories for this package downward
class App { public static void main(String[] a) { SpringApplication.run(App.class, a); } }
You only add an explicit @EnableJpaRepositories(basePackages = "...") when repositories live
outside the main application package or you need multiple datasources. The common failure —
"no repository bean found" — almost always means the interface sits outside the scanned package
tree.
Rule of thumb: Keep repositories under the main application package for zero config; add
explicit @EnableJpaRepositories(basePackages=...) only when they live elsewhere.
getReferenceById (formerly getOne) returns a lazy proxy without hitting the database — it
assumes the row exists and defers loading until you access a non-id property.
// Setting a FK association without loading the parent:
Order order = new Order();
order.setCustomer(customerRepo.getReferenceById(customerId)); // no SELECT — just sets the FK
orderRepo.save(order); // INSERT uses customerId
It's an optimization when you only need the id — e.g. assigning a relationship — and want to
skip a wasted SELECT. The catch: if the id doesn't exist, you get an EntityNotFoundException
later (on property access or flush), not immediately. findById loads eagerly and returns an
Optional.
Rule of thumb: Use getReferenceById to wire up a foreign key without a SELECT; use findById
when you actually need the entity's data or must verify it exists now.
More Data Access interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.