skip to content

You're setting a team convention for which repository interface to extend. Walk through the trade-offs across the hierarchy from Repository up to JpaRepository.

level: principalimportance: should knowfreq 25%

answer

  1. interface = public API/contract
  2. narrowest that fits > default JpaRepository
  3. Repository = read-only/CQRS surface
  4. JpaRepository = flush/batch/getReferenceById, store-coupled
  5. cross-cutting via @NoRepositoryBean base or fragments

basics

~20 s

Extend the narrowest interface that exposes only the operations a repository should offer: Repository for a minimal custom surface, CrudRepository/ListCrudRepository for CRUD, PagingAndSortingRepository for read/paging, JpaRepository when you need JPA extras like flush and batch. Broader interfaces mean broader, harder-to-govern APIs.

solid answer

~50 s

The hierarchy is a menu of capabilities, and the interface you extend becomes the repository's public API — so choose by exposure, not convenience. Repository<T, ID> gives a blank slate: you declare only the methods you want (great for read-only or tightly-scoped repositories). CrudRepository/ListCrudRepository add full CRUD (List variants for ergonomic List returns). PagingAndSortingRepository/ListPagingAndSortingRepository add sorting and Page-based pagination — and since Spring Data 3.0 they no longer imply CRUD, so you compose them. JpaRepository sits at the top: it composes the List variants plus JPA-specific power (flush, saveAndFlush, getReferenceById, deleteAllInBatch, QueryByExampleExecutor) — convenient but store-coupled and broad. Defaulting everything to JpaRepository is the common but questionable choice: it leaks persistence details, tempts misuse (flush/batch in the wrong layer), and ties you to JPA. A principled convention: narrowest interface per repository; introduce a @NoRepositoryBean base for genuinely cross-cutting methods.

code

java · 17 lines
java
// Read model — deny mutation at the type level:
public interface UserSummaryRepository extends Repository<User, Long> {
    Optional<UserSummary> findSummaryByEmail(String email);
    Page<UserSummary> findAllByActiveTrue(Pageable pageable);
}

// Mutating aggregate that needs CRUD + paging, store-portable, no JPA leak:
public interface OrderRepository
        extends ListCrudRepository<Order, Long>,
                ListPagingAndSortingRepository<Order, Long> {
    List<Order> findByCustomerId(Long customerId);
}

// Reserve JpaRepository for the rare case that truly needs flush/batch:
public interface BulkImportRepository extends JpaRepository<ImportRow, Long> {
    // deliberately uses saveAllAndFlush(...) / deleteAllInBatch(...)
}

go deeper

for a junior

Recognize JpaRepository as the fullest interface and CrudRepository as basic CRUD.

for a middle

Explain each level's capabilities and that JpaRepository adds flush/batch on top of CRUD + paging.

for a senior

Argue for narrowest-fit interfaces, store portability via Commons interfaces, and read-only surfaces via Repository.

for a principal

Set a governance convention balancing exposure, portability, and cross-cutting behavior via @NoRepositoryBean bases/fragments versus default JpaRepository convenience.

## Frame: the interface *is* the contract Whatever repository interface you extend defines the **public API** that services and the rest of the app can call. Repository interfaces are injected widely, so a broad base = a broad, hard-to-govern surface. The design question is *capability exposure*, not typing convenience. ## The menu, narrowest to broadest 1. **`Repository<T, ID>`** — empty marker. You get **nothing** except what you declare. Ideal for: - Read-only repositories (only `findByX`/`@Query` reads, no `save`/`delete` exposed). - CQRS-style query repositories. - Any place you want to *deny* mutation at the type level. Strongest encapsulation. 2. **`CrudRepository<T, ID>`** — full CRUD, collection methods return `Iterable<T>`. 3. **`ListCrudRepository<T, ID>`** — same CRUD, collection methods return `List<T>` (Spring Data 3.0+). Prefer over `CrudRepository` when you want CRUD and `List` ergonomics without JPA specifics. 4. **`PagingAndSortingRepository<T, ID>`** — `findAll(Sort)` + `findAll(Pageable)->Page`. Since 3.0 it **does not** include CRUD, so compose it with a CRUD interface if you need both. Use alone for a **read + paginate** surface that withholds mutation. 5. **`ListPagingAndSortingRepository<T, ID>`** — as above, `findAll(Sort)` returns `List`. 6. **`JpaRepository<T, ID>`** — the top. Composes `ListCrudRepository` + `ListPagingAndSortingRepository` + `QueryByExampleExecutor`, and adds **JPA-specific** methods: `flush()`, `saveAndFlush()`, `saveAllAndFlush()`, `deleteAllInBatch()`, `deleteAllByIdInBatch()`, `getReferenceById()` (lazy proxy). Powerful but: - **Store-coupled**: names/semantics are JPA-specific; you can't swap to Mongo without changing the interface. - **Leaks persistence concerns** upward: `flush`/batch/`getReferenceById` are transaction/EM-management details that generally shouldn't be callable from service or web layers. - **Broad surface**: everything is exposed, inviting misuse. ## Trade-off analysis for a team convention - **Default-to-JpaRepository** (the common shop convention): maximum convenience, minimum thought, but weakest governance — every repository can flush and batch-delete from anywhere. Fine for small teams/CRUD apps; risky at scale. - **Narrowest-fit convention**: each repository extends the least-capable interface that satisfies its real callers. Read models extend `Repository`; mutating aggregates extend `ListCrudRepository`; paginated lists add `PagingAndSortingRepository`; reserve `JpaRepository` for the rare repository that genuinely needs `flush`/batch. Higher upfront discipline, far better encapsulation and refactorability. - **Portability**: if store-independence matters (or you use multiple stores), stay in **Commons** interfaces (`Repository`…`ListPagingAndSortingRepository`) and avoid `JpaRepository`. ## Cross-cutting behavior For methods every repository needs (soft delete, tenant scoping, `getByIdOrThrow`), introduce a **`@NoRepositoryBean` base interface** (optionally backed by a custom base class via `repositoryBaseClass`) rather than widening everyone to `JpaRepository`. Alternatively use **fragment interfaces** (custom `*Impl`) for behavior that needs the `EntityManager`, keeping the public repository interface narrow. ## Gotchas that inform the decision - Post-3.0, `PagingAndSortingRepository` no longer drags in CRUD — composition is now explicit and intentional (a feature, not a nuisance). - `getReferenceById` returns a lazy proxy (formerly `getById`/`getOne`); exposing it invites `LazyInitializationException` outside a session. - `Page` totals cost an extra count query — expose `Slice`-returning derived queries where totals aren't needed. ## Bottom line Treat the hierarchy as capability composition. Prefer the narrowest interface; escalate to `JpaRepository` only when a concrete need (flush/batch/reference proxy) justifies the exposure and store coupling.

  • What concrete risks come from defaulting every repository to JpaRepository?
    It exposes flush/batch/getReferenceById everywhere, leaking transaction and EntityManager concerns into service/web layers, invites misuse and LazyInitializationException, and couples you to JPA, hurting store portability and refactorability.
  • You need a getByIdOrThrow on every repository. Widen the base to JpaRepository or something else?
    Introduce a @NoRepositoryBean base interface carrying the shared method (optionally with a custom base class via repositoryBaseClass), so each concrete repo inherits it without widening the exposed CRUD/JPA surface unnecessarily.
  • When is defaulting to JpaRepository actually reasonable?
    Small teams or straightforward CRUD apps on a single JPA store where governance overhead outweighs the encapsulation benefit — a pragmatic, conscious trade-off rather than an accident.

saying these in an interview costs you the question

  • 'Always extend JpaRepository, it's the standard' with no trade-off awareness
  • Not realizing the interface defines the exposed API surface
  • Unaware PagingAndSortingRepository no longer implies CRUD in 3.0
  • Recommending JpaRepository for multi-store/portable code

context