When should you extend JpaRepository versus compose custom repository fragments?
answer
- extend = generated CRUD/derived/@Query
- fragment = interface + Impl for imperative logic
- Impl postfix convention (configurable)
- compose multiple fragments to share/override
- narrow base = interface segregation, hide bulk delete
basics
~20 sExtend JpaRepository for standard CRUD, derived queries and @Query methods. Add a custom fragment (an interface plus an Impl) when a method needs hand-written logic — EntityManager, Criteria API, QueryDSL — that derived queries and @Query can't express. Compose narrow fragments to restrict the exposed API.
solid answer
~50 sExtend JpaRepository (or a narrower base) for everything Spring Data can generate: CRUD, paging, derived queries, @Query. When a method needs imperative implementation — dynamic Criteria queries, EntityManager tuning, QueryDSL, batch JDBC, or logic no query DSL captures — define a *fragment*: a custom interface plus an implementation class named with the configured postfix (default Impl, e.g. UserRepositoryCustom + UserRepositoryCustomImpl). Your repository extends both JpaRepository and the fragment; Spring Data weaves the generated CRUD proxy together with your Impl into one bean. You can compose *several* fragments to share behaviour across repositories and to keep each concern small. Composition also enables interface segregation: extend the bare Repository marker and expose only chosen methods, or build a read-only repository, instead of leaking JpaRepository's full mutating surface everywhere. Rule of thumb: extend for the generated 80%, compose fragments for the bespoke 20% and for deliberately narrow APIs.
code
java · 26 lines// Fragment for imperative Criteria-API logic
interface UserSearchFragment {
List<User> search(UserFilter filter);
}
class UserSearchFragmentImpl implements UserSearchFragment { // 'Impl' postfix is mandatory
@PersistenceContext EntityManager em;
public List<User> search(UserFilter f) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> u = q.from(User.class);
List<Predicate> ps = new ArrayList<>();
if (f.email() != null) ps.add(cb.equal(u.get("email"), f.email()));
if (f.active() != null) ps.add(cb.equal(u.get("active"), f.active()));
return em.createQuery(q.where(ps.toArray(Predicate[]::new))).getResultList();
}
}
// Compose generated CRUD + the custom fragment into one repository
interface UserRepository extends JpaRepository<User, Long>, UserSearchFragment {
List<User> findByActiveTrue(); // derived — still just 'extend'
}
// Interface segregation: a read-only repo that never exposes delete/saveAll
interface UserReadRepository extends Repository<User, Long> {
Optional<User> findById(Long id);
List<User> findAll();
}go deeper
Know you extend JpaRepository for CRUD and add @Query for custom SQL.
Explain fragment interface + Impl for logic that derived queries/@Query can't express.
Cover the Impl naming convention, multiple fragments, and overriding generated methods.
Frame it as interface-segregation and API-surface design: choose the narrowest base, compose shared fragments, and prevent leaking dangerous bulk operations across the codebase.
**The mental model.** A Spring Data repository is assembled from *fragments*. The base fragment is the generated one (backed by `SimpleJpaRepository`) that provides CRUD/paging when you extend `JpaRepository`/`CrudRepository`. You can add your own fragments — each a small interface + implementation — and Spring Data composes all of them into a single proxy bean at startup. "Extend vs compose" is really "use the generated fragment vs add custom fragments", and they are not mutually exclusive. **When extending is enough.** Most access is expressible declaratively: - CRUD & paging: inherited from `JpaRepository`. - **Derived query methods**: `findByEmailAndActiveTrue(...)` — Spring parses the method name into a query. - **`@Query`**: explicit JPQL or native SQL, including `@Modifying` updates/deletes. - **Query by Example, Specifications** (`JpaSpecificationExecutor`), or **QueryDSL** (`QuerydslPredicateExecutor`) mixed in for dynamic-but-structured queries. If your need fits any of these, just extend — no Impl class required. **When to compose a custom fragment.** Reach for a fragment when a method needs *imperative* code: - Dynamic queries built with the **JPA Criteria API** or a raw `EntityManager` you inject. - Multi-step logic, native batch operations, calling stored procedures with custom handling, or integrating another data source. - Behaviour that would be ugly or impossible as a single `@Query`. **The mechanics (naming matters).** ```java interface UserRepositoryCustom { // the fragment interface List<User> searchDynamic(UserFilter f); } class UserRepositoryCustomImpl // MUST end with the postfix (default 'Impl') implements UserRepositoryCustom { @PersistenceContext private EntityManager em; public List<User> searchDynamic(UserFilter f) { /* Criteria API */ } } interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {} ``` Spring Data finds `UserRepositoryCustomImpl` by convention: `<fragmentInterface> + Impl`. The postfix is configurable via `@EnableJpaRepositories(repositoryImplementationPostfix = "...")`. The final `UserRepository` bean exposes CRUD + the custom method as one interface; the proxy dispatches each call to the fragment that implements it. **Composing multiple fragments.** A repository can extend several fragment interfaces, each with its own Impl. This lets you: - **Share behaviour**: a reusable `ContainsFullTextSearch<T>` fragment implemented once and mixed into many repositories. - **Keep concerns small** and independently testable. - **Override generated methods**: a fragment method with the same signature as a base method takes precedence (fragment order determines resolution), letting you customise, e.g., a special `save`. **Interface segregation / restricting the API — the design lever.** Extending `JpaRepository` publishes its *entire* mutating surface (`deleteAll`, `saveAll`, batch deletes…) to every caller. Sometimes that's undesirable. Composition lets you narrow: - Extend the bare `Repository<T,ID>` marker and declare only the handful of methods you want (they're still generated). - Or define a `@NoRepositoryBean` intermediate base with the exact method set, and have concrete repositories extend it. - Build a truly read-only repository by exposing only `findById`/`findAll`/derived reads. This matters at the *architecture* level: it prevents accidental bulk deletes, keeps the persistence API intentional, and makes the repository contract self-documenting. **Decision guide.** - Standard CRUD/paging → extend `JpaRepository`. - Structured dynamic queries → add `JpaSpecificationExecutor`/`QuerydslPredicateExecutor` (still "extend"). - Imperative/custom implementation → **compose a fragment** (interface + Impl). - Reuse custom behaviour across repositories → **compose a shared fragment**. - Deliberately limited API (read-only, no bulk delete) → **compose/segregate** off a narrower base instead of extending JpaRepository. **Gotchas.** (1) The Impl naming convention is strict — a mismatched postfix means Spring silently doesn't find your implementation and you get 'no property/abstract method' errors. (2) Fragment Impls are Spring beans and can inject dependencies, but keep them stateless. (3) Don't over-engineer: if `@Query` expresses it clearly, a fragment is needless indirection.
- How does Spring Data find the implementation of a custom fragment interface?By naming convention: the fragment interface name plus a configurable postfix (default 'Impl'), e.g. UserSearchFragment -> UserSearchFragmentImpl. Set repositoryImplementationPostfix on @EnableJpaRepositories to change it. A mismatch means the Impl isn't detected and startup/first-call fails.
- Two fragments both define a method with the same signature — which wins?Fragment declaration order in the repository interface decides precedence; the first-listed fragment providing the method wins. This is also how a fragment can override a generated base method like save.
- Why might you avoid extending JpaRepository at all for a given aggregate?To restrict the API surface — extending JpaRepository publishes deleteAll, batch deletes, saveAll etc. to every caller. Extending the bare Repository marker (or a @NoRepositoryBean base) lets you expose only the intended methods, e.g. a read-only repository.
saying these in an interview costs you the question
- Thinking you need a fragment/Impl for ordinary CRUD or @Query methods
- Naming the Impl class arbitrarily (breaking the postfix convention)
- Believing extend and compose are mutually exclusive
- Assuming extending JpaRepository is always the right base regardless of API surface