- Chapter
- Chapter 1 — Spring Data Fundamentals
- Prerequisites
- Topic 1.1; Volume II (proxies, AOP, FactoryBean)
- You will be able to
- Explain how Spring Data turns interfaces into beans, how the repository proxy dispatches calls, how queries are generated or looked up, and how to extend repositories with fragments
- Covers
- Repository Interfaces · Repository Proxy · Query Generation
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally
- 4A. Deep Dive — Custom Fragments and Base Classes
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
Spring Data's architecture turns a declared interface into a working repository at start-up. Three parts cooperate:

| Part | Role | Key types |
|---|---|---|
| Repository interfaces | Your contract: base interface + finder methods + optional fragments | Repository<T, ID>, CrudRepository, JpaRepository, custom fragments |
| Repository proxy | Runtime implementation created per interface | RepositoryFactorySupport, JpaRepositoryFactoryBean, JDK dynamic proxy |
| Query generation | Turning methods into executable queries | QueryLookupStrategy, PartTree, @Query, named queries |
2Why It Exists
Before Spring Data, each entity needed a DAO class with repetitive EntityManager code: find by id, save, delete, a handful of finder queries. Spring Data (2011, from the Hades project) replaced this boilerplate with declarative interfaces: common operations come from a shared base implementation, finders are derived from method names or annotations, and anything special can be added as a fragment. The same architecture serves JPA, JDBC, MongoDB, Redis, Elasticsearch and more — each module supplies its own factory and query engine.
3Core Concepts
3.1 Repository interfaces
public interface OrderRepository
extends ListCrudRepository<Order, UUID>, // base contract
ListPagingAndSortingRepository<Order, UUID>,
OrderSearchFragment { // custom fragment
// derived
List<Order> findByCustomerIdAndStatus(String customerId, OrderStatus status);
// declared
@Query("select o from Order o where o.total > :min order by o.total desc")
List<Order> bigOrders(BigDecimal min, Limit limit);
}| Element | Resolution |
|---|---|
| Methods from base interfaces | SimpleJpaRepository (base implementation) |
| Derived finder methods | Query created from the method name |
@Query / named query methods | Declared JPQL or native SQL |
| Fragment methods | Your implementation class (…Impl) |
| Default methods | Executed directly on the proxy |
Repository interfaces are discovered by scanning (Boot auto-configures @EnableJpaRepositories for the application package); @NoRepositoryBean marks intermediate interfaces that must not become beans.
3.2 Repository proxy

| Proxy layer | Responsibility |
|---|---|
| Default method invoker | Runs Java default methods |
| Transaction interceptor | @Transactional from SimpleJpaRepository (read-only for finders) and your annotations |
| Exception translation | PersistenceException → Spring DataAccessException (via @Repository semantics) |
| Query executor interceptor | Executes derived/declared queries |
| Implementation dispatch | Routes base and fragment methods to their target objects |

3.3 Query generation

Strategy (queryLookupStrategy) | Behaviour |
|---|---|
CREATE_IF_NOT_FOUND (default) | Use declared query (@Query, named query) if present; otherwise derive from the method name |
USE_DECLARED_QUERY | Declared queries only; fail otherwise |
CREATE | Always derive from the name |
Derived queries are parsed by PartTree into subject (find…By, count…By, exists…By, delete…By) and predicate parts (CustomerIdAndStatus), then turned into JPA Criteria queries (Chapter 2).
4How It Works Internally
1. Registration: JpaRepositoriesRegistrar scans base packages for interfaces extending Repository; for each, it registers a bean definition of type JpaRepositoryFactoryBean (a Spring FactoryBean).
2. Factory creation: on first use (or eagerly at start-up), the factory bean creates a JpaRepositoryFactory with the EntityManager (shared, transaction-aware proxy) and repository metadata (domain type, id type).
3. Target: the factory instantiates the base implementation (SimpleJpaRepository or a custom base class) and discovers fragments (OrderSearchFragmentImpl beans by naming convention).
4. Query methods: for each query method, the lookup strategy resolves a RepositoryQuery (PartTreeJpaQuery, SimpleJpaQuery, NativeJpaQuery, named query) — parsing and validating at start-up, so invalid method names fail fast.
5. Proxy: a ProxyFactory builds a JDK dynamic proxy for the interface with advisors: exception translation, transactions, QueryExecutorMethodInterceptor and implementation dispatch.
6. Invocation: the proxy routes each call; query methods bind parameters, apply paging/sorting/projections and execute via the EntityManager.
4ADeep Dive — Custom Fragments and Base Classes
Fragment (behaviour for one or a few repositories):
public interface OrderSearchFragment {
List<OrderSummary> search(OrderSearch criteria);
}
// "Impl" postfix convention
class OrderSearchFragmentImpl implements OrderSearchFragment {
private final EntityManager em;
OrderSearchFragmentImpl(EntityManager em) { this.em = em; }
@Override
public List<OrderSummary> search(OrderSearch c) {
var cb = em.getCriteriaBuilder();
var q = cb.createQuery(OrderSummary.class);
var o = q.from(Order.class);
// ... build predicates from criteria ...
return em.createQuery(q).setMaxResults(c.limit()).getResultList();
}
}Custom base class (behaviour for all repositories):
@NoRepositoryBean
public interface BaseRepository<T, ID> extends ListCrudRepository<T, ID> {
T getRequired(ID id); // throws if absent
}
public class BaseRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>
implements BaseRepository<T, ID> {
public BaseRepositoryImpl(JpaEntityInformation<T, ?> info, EntityManager em) {
super(info, em);
}
@Override public T getRequired(ID id) {
return findById(id)
.orElseThrow(() -> new EntityNotFoundException(String.valueOf(id)));
}
}
@Configuration
@EnableJpaRepositories(repositoryBaseClass = BaseRepositoryImpl.class)
class JpaConfig { }| Extension | Scope | Use for |
|---|---|---|
| Query methods | One repository | Simple finders |
| Fragment | Selected repositories (reusable) | Complex queries, Criteria/Querydsl, batch operations |
| Custom base class | All repositories | Cross-cutting operations (getRequired, soft delete helpers) |
| Default methods | One repository | Convenience wrappers around other methods |
5APIs / Features / Syntax
| API / property | Purpose |
|---|---|
@EnableJpaRepositories (auto-configured by Boot) | Scanning, base class, lookup strategy, postfix |
@NoRepositoryBean | Exclude intermediate interfaces |
JpaRepositoryFactoryBean, JpaRepositoryFactory | Repository creation |
SimpleJpaRepository | Default base implementation |
RepositoryQuery, PartTree, QueryLookupStrategy | Query resolution |
spring.data.jpa.repositories.bootstrap-mode | default, deferred, lazy initialisation |
spring.data.jpa.repositories.enabled | Toggle repository auto-configuration |
6Practical Examples
6.1 Seeing the proxy
@Test
void repositoryIsAProxy(@Autowired OrderRepository repo) {
assertThat(AopUtils.isJdkDynamicProxy(repo)).isTrue();
assertThat(AopProxyUtils.ultimateTargetClass(repo))
.isEqualTo(SimpleJpaRepository.class);
}6.2 Start-up validation of query methods
Caused by: org.springframework.data.repository.query.QueryCreationException:
Could not create query for public abstract java.util.List
com.acme.OrderRepository.findByCustomer(java.lang.String);
Reason: No property 'customer' found for type 'Order'; Did you mean 'customerId'6.3 Deferred bootstrap for faster start-up
spring:
data:
jpa:
repositories:
# repositories initialised in the background after context refresh
bootstrap-mode: deferred6.4 Reusable fragment across repositories
public interface SoftDeleteFragment<T> { void softDelete(T entity); }
public interface CustomerRepository extends ListCrudRepository<Customer, UUID>,
SoftDeleteFragment<Customer> { }
public interface SupplierRepository extends ListCrudRepository<Supplier, UUID>,
SoftDeleteFragment<Supplier> { }7Common Problems & Mistakes
- Intermediate interfaces without @NoRepositoryBean — Spring tries to create a repository for them.
- Misnamed fragment implementations — start-up failures or derived-query attempts.
- Assuming repository methods are free — each call goes through proxies, transactions and SQL.
- Business logic in custom base classes — keep base classes generic.
- Repositories for every table instead of aggregates.
Several further problems only appear under real data volumes. Derived method names that keep growing, such as findByCustomerIdAndStatusAndCreatedAtAfterOrderByTotalDesc, are a sign that the query belongs in @Query or a fragment, where it can be read and tuned. save() on an entity that is not new calls EntityManager.merge(), which returns a different managed instance; code that keeps using the original object loses later changes. Derived deleteBy methods load every matching entity and remove them one by one, firing lifecycle callbacks, so a bulk delete of thousands of rows needs a @Modifying @Query instead. Calling a repository inside a loop from code without a surrounding @Transactional opens one transaction per call. Finally, returning entities from finders used only for display wastes memory; interface or record projections fetch just the columns needed.
8Performance & Production Considerations
| Concern | Guidance |
|---|---|
| Start-up time | Many repositories and queries parse at start-up; deferred/lazy bootstrap or AOT repositories (Topic 3.1) help |
| Proxy overhead | Negligible compared with database access |
| Transactions | Each repository call outside a service transaction opens its own transaction |
| Observability | Enable SQL logging/metrics per repository method in tests and staging |
9Architecture & Design Considerations
- Interface-first persistence: repositories are ports; Spring Data provides adapters.
- Fragments over inheritance for reusable capabilities.
- Module-per-store configuration in polyglot applications (separate
@Enable…Repositoriespackages). - Keep complex reads in dedicated query components where derived queries become unreadable.
10Interview Questions & Answers
How does Spring Data create an implementation for a repository interface?
It generates a proxy at start-up. Boot's auto-configuration, or @EnableJpaRepositories, scans the base packages for interfaces extending Repository and registers a JpaRepositoryFactoryBean definition for each one. When that factory bean is initialised, it creates a JpaRepositoryFactory with a shared, transaction-aware EntityManager and the repository's metadata: domain type, id type and declared methods. The factory instantiates the base implementation, normally SimpleJpaRepository, discovers custom fragment implementations, and resolves a RepositoryQuery for every query method, validating them early. Finally it builds a JDK dynamic proxy for the interface with interceptors for exception translation, transactions, query execution and dispatch to the base or fragment implementation. The bean you inject is that proxy; you never write an implementation class.
What is SimpleJpaRepository?
SimpleJpaRepository is Spring Data JPA's default base implementation, the class that actually implements the methods inherited from CrudRepository, ListCrudRepository, PagingAndSortingRepository and JpaRepository, such as save, findById, findAll with paging, deleteById and count. Every repository proxy delegates those calls to an instance of it, created per repository with the entity's metadata and an EntityManager. It is annotated @Transactional(readOnly = true) at class level, with write methods overriding that, which is why repository calls are transactional even without a service transaction. One detail worth knowing: save() calls persist() for new entities and merge() otherwise, deciding newness from the id, a version attribute or the Persistable interface, so always use the returned instance.
What is the default query lookup strategy?
The default is CREATE_IF_NOT_FOUND. For each query method, Spring Data first looks for a declared query: an @Query annotation on the method, or a named query matching the convention Entity.methodName, for example in orm.xml or a @NamedQuery. If one exists it is used; otherwise the query is derived from the method name by the PartTree parser. The alternatives are USE_DECLARED_QUERY, which fails start-up if a method has no declared query, and CREATE, which always derives from the name and ignores named queries. The strategy is set with the queryLookupStrategy attribute of @EnableJpaRepositories. The default suits most teams; USE_DECLARED_QUERY suits teams that want every query visible and reviewed.
What does @NoRepositoryBean do?
@NoRepositoryBean tells Spring Data not to create a repository bean for an interface that would otherwise be picked up by scanning. It is meant for intermediate interfaces, such as a generic BaseRepository<T, ID> that adds shared methods for all your repositories. Without it, Spring Data tries to create a repository for the generic interface itself, cannot determine a concrete domain type, and fails at start-up. Spring Data uses the same annotation on its own hierarchy: CrudRepository, ListCrudRepository, PagingAndSortingRepository and JpaRepository are all marked @NoRepositoryBean. Concrete repositories that extend the intermediate interface with real type arguments, such as OrderRepository extends BaseRepository<Order, UUID>, are still detected and created normally.
How do you add custom query logic to a repository?
Use a repository fragment. Declare an interface with the custom methods, for example OrderSearchFragment with search(OrderSearch criteria), and implement it in a class named after the interface with the Impl postfix, OrderSearchFragmentImpl. The implementation is a normal Spring bean, so it can have an EntityManager, JdbcClient or Querydsl factory injected through its constructor and use Criteria, native SQL or batch operations. Then let the repository interface extend the fragment interface as well as its base interface. The proxy routes fragment methods to your implementation, and fragment methods even take precedence over base methods with the same signature, so you can customise save. A misnamed Impl class makes Spring try to derive a query instead.
When are invalid derived query methods detected?
Normally at start-up, when the repository is created. The lookup strategy resolves every query method: derived methods are parsed by PartTree and their properties checked against the entity model, so findByCustomer on an entity without a customer property fails with a QueryCreationException naming the method. Declared JPQL in @Query is also validated, because Hibernate parses it when the query is created. Two exceptions matter. Native queries are not validated until they execute, so only tests catch SQL errors. And with spring.data.jpa.repositories.bootstrap-mode set to lazy, repositories are initialised on first use, so the error moves to the first request. Deferred mode still initialises them before the application is ready.
How would you add an operation to all repositories?
Create a custom base repository. Define a generic interface marked @NoRepositoryBean, for example BaseRepository<T, ID> extending ListCrudRepository<T, ID>, with the shared method such as getRequired(ID id). Implement it in a class that extends SimpleJpaRepository<T, ID> and has a constructor taking JpaEntityInformation and EntityManager, which Spring Data calls reflectively. Register it with @EnableJpaRepositories(repositoryBaseClass = BaseRepositoryImpl.class), and let concrete repositories extend BaseRepository. Every repository proxy then uses your class as its target, so the method is available everywhere. Keep base classes generic, such as soft-delete helpers or required lookups. If only some repositories need the behaviour, a reusable generic fragment is the more flexible choice than changing every repository's base.
How does Spring Data's architecture support ports-and-adapters design?
In ports-and-adapters terms, the repository interface is a port: a domain-owned contract expressed in domain language, such as findOverdueInvoices or save(Order), with aggregate roots as types. Spring Data generates the adapter at runtime from that contract, so the domain never sees EntityManager, SQL or driver APIs. Fragments and custom base classes extend the adapter side when generated queries are not enough, again without changing the port. The architecture holds only if interfaces stay domain-focused: exposing JPA-specific types, Specification or Pageable deep in the domain leaks the adapter. Done well, you can move an aggregate from JPA to JDBC or a document store by changing the adapter module, and test the domain with in-memory implementations.