At runtime, when you call a method on the generated repository proxy, how does it decide what to actually execute? And what is resolved eagerly at startup versus per call?
answer
- QueryExecutorMethodInterceptor classifies each call
- 4 buckets: base / query / custom fragment / default
- RepositoryQuery built + validated at startup
- QueryLookupStrategy: CREATE / USE_DECLARED / CREATE_IF_NOT_FOUND
- Self-invocation bypasses transaction advice
basics
~20 sEvery call goes through the proxy's QueryExecutorMethodInterceptor, which classifies the method: a declared query method runs a pre-built RepositoryQuery; a custom-fragment or default method goes to that implementation; a base CRUD method goes to SimpleJpaRepository. Query objects and metadata are built at startup; dispatch happens per call.
solid answer
~50 sThe JDK proxy routes each invocation through `QueryExecutorMethodInterceptor`. Using the `RepositoryInformation`/`RepositoryComposition` computed at startup, it classifies the invoked method into one of: (1) a **store query method** — dispatched to a `RepositoryQuery` object (derived from the name or from `@Query`) that was created and cached at bootstrap; (2) a **custom fragment method** — forwarded to your `…Impl` fragment; (3) an **interface default method** — invoked directly on the interface; (4) a **base CRUD method** — forwarded to the target `SimpleJpaRepository`. What's eager: interface metadata, the fragment composition, and — where possible — the `RepositoryQuery` instances and their validation (via `QueryLookupStrategy`, honoring `CREATE`, `USE_DECLARED_QUERY`, or `CREATE_IF_NOT_FOUND`). What's per-call: the classification lookup (cached), binding actual arguments, executing the query, and applying advice (transaction, exception translation). This is why bad `@Query`/derived names usually fail at startup, not first use.
code
java · 28 linespublic interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
// (2) STORE QUERY METHOD — RepositoryQuery derived from the name at startup
Optional<User> findByEmail(String email);
// (2) STORE QUERY METHOD — RepositoryQuery from the declared @Query, validated at startup
@Query("select u from User u where u.active = true and u.tenantId = :t")
List<User> findActiveForTenant(@Param("t") long tenantId);
// (3) DEFAULT METHOD — executed as-is on the interface, may call other methods
default Optional<User> findByEmailOrThrow(String email) {
return Optional.of(findByEmail(email)
.orElseThrow(() -> new IllegalStateException("no user")));
}
// (1) save/findById/delete... -> forwarded to SimpleJpaRepository (base target)
}
interface UserRepositoryCustom { // (4) custom fragment contract
List<User> searchFuzzy(String q);
}
class UserRepositoryImpl implements UserRepositoryCustom { // fragment impl (…Impl postfix)
@PersistenceContext EntityManager em;
@Override public List<User> searchFuzzy(String q) {
// hand-written; proxy forwards searchFuzzy(...) here
return em.createQuery("...", User.class).getResultList();
}
}go deeper
Not expected to know runtime dispatch internals.
May know 'derived queries vs @Query' but not the interceptor or fragment routing.
Explains QueryExecutorMethodInterceptor classification and eager query building.
Reasons about fail-fast vs bootstrap-mode trade-offs, composition model, per-call cost, and self-invocation pitfalls.
**The dispatcher.** The repository proxy's central advice is `QueryExecutorMethodInterceptor` (spring-data-commons). It wraps every method call and answers one question: *what backs this method?* The decision uses metadata built once at startup and cached, so per-call cost is a map lookup plus execution. **The four method categories.** 1. **Base (CRUD/paging) methods** — declared by `CrudRepository`/`JpaRepository`/`PagingAndSortingRepository` (`save`, `findById`, `findAll`, `delete`, `count`, …). These are forwarded to the **target base implementation**, `SimpleJpaRepository`, which uses the `EntityManager`. 2. **Store query methods** — methods *you* declare that aren't CRUD (`findByEmail`, `findByLastNameOrderByCreatedAtDesc`, `@Query(...) List<X> report(...)`). Each is backed by a `RepositoryQuery` object built at startup. How it's built is decided by the **`QueryLookupStrategy`**: - `CREATE`: always derive from the method name (parse `findBy…`, `And`, `OrderBy`, etc.). - `USE_DECLARED_QUERY`: require an explicit `@Query` or named query; fail if none. - `CREATE_IF_NOT_FOUND` (default): prefer a declared query, else derive from the name. 3. **Custom fragment methods** — methods implemented by *you* in a fragment class (e.g. `UserRepositoryCustom` + `UserRepositoryImpl`, or any `RepositoryFragment`). The proxy forwards to the fragment instance stored in the `RepositoryComposition`. 4. **Interface `default` methods** — Java 8 `default` methods on the repository interface are executed as-is (invoked on the proxy/interface), letting you compose calls to other repository methods. **Eager (startup) vs. lazy (per call).** - *Eager at bootstrap (default `BootstrapMode.DEFAULT`)*: reflectively inspect the interface → `RepositoryMetadata`/`RepositoryInformation`; assemble the `RepositoryComposition` (base + fragments); build and **validate** `RepositoryQuery` objects for query methods (this is where an invalid JPQL `@Query` or a `findByNonexistentProperty` blows up — *fail fast*); create the `ProxyFactory`, run post-processors, add interceptors, and materialize the JDK proxy. - *Per call*: `QueryExecutorMethodInterceptor` looks up the method's category (cached), and for a query method binds the actual arguments into the pre-built query and executes it; base methods hit `SimpleJpaRepository`; then the advisor chain (transaction start/commit, persistence-exception translation) wraps the call. **Why the design matters (principal-level framing).** - *Fail-fast*: eager query construction means most repository mistakes surface at context startup, improving deployment safety — at the cost of startup latency. `BootstrapMode.LAZY`/`DEFERRED` trade that away for faster/async startup (moving both proxy creation and query validation later), which changes *when* you learn about a broken query. - *Composition over inheritance*: since Spring Data 2.0, a repository is a *composition* of fragments rather than a single subclass, which is what lets custom impls, default methods, base CRUD, and multiple `Repository` interfaces coexist behind one proxy. - *Cost model*: per-call overhead is a classification lookup + AOP advice + query execution; the proxy itself is cheap. At very large repository counts, the dominant startup cost is metadata + query building, which is exactly what `DEFERRED` bootstrap parallelizes. **Gotchas.** - Calling a repository method from *within* a `default` method or another repository method does **not** re-enter the proxy's advice unless the call goes back through the proxy reference — self-invocation on the target bypasses transaction advice, same as ordinary Spring AOP self-invocation. - A method name typo that still parses as a valid property path can silently produce a *different* query; derivation is only as safe as your property names. - `Object` methods (`toString`, `equals`, `hashCode`) and `Repository`-marker methods are handled specially by the interceptor, not treated as queries.
- Why does a malformed @Query usually fail at startup rather than on first call?With the default bootstrap mode, RepositoryQuery objects are built and validated eagerly during proxy creation via QueryLookupStrategy, so parse/validation errors surface at context refresh — fail fast.
- How can a custom fragment method and a derived query method coexist behind one proxy?Since Spring Data 2.0 a repository is a RepositoryComposition of fragments; QueryExecutorMethodInterceptor routes each method to the right fragment — your …Impl for custom methods, the base SimpleJpaRepository for CRUD, and pre-built RepositoryQuery objects for query methods.
- If a default method calls findByEmail, does that inner call get transaction/exception-translation advice?Only if it goes back through the proxy. A plain internal call on the target instance is self-invocation and bypasses the AOP advice chain, just like ordinary Spring AOP self-invocation.
saying these in an interview costs you the question
- Thinking each query is parsed fresh on every call
- Believing custom-impl methods go through SimpleJpaRepository
- Assuming self-invocation from a default method is still transactionally advised
- Claiming derived queries are validated only at first use in all bootstrap modes