skip to content

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?

level: principalimportance: nice to knowfreq 22%

answer

  1. QueryExecutorMethodInterceptor classifies each call
  2. 4 buckets: base / query / custom fragment / default
  3. RepositoryQuery built + validated at startup
  4. QueryLookupStrategy: CREATE / USE_DECLARED / CREATE_IF_NOT_FOUND
  5. Self-invocation bypasses transaction advice

basics

~20 s

Every 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 s

The 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 lines
java
public 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

for a junior

Not expected to know runtime dispatch internals.

for a middle

May know 'derived queries vs @Query' but not the interceptor or fragment routing.

for a senior

Explains QueryExecutorMethodInterceptor classification and eager query building.

for a principal

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

context