ArchitectMind
  1. Home
  2. Tutorials
  3. Book 4 · Spring & Spring Boot
  4. Volume V
  5. Spring Data Architecture
Book 4 · Volume V — Spring Data, JPA & Persistence · Topic 1.2

Spring Data Architecture

Repository interfaces, repository proxy and query generation

8 min readStage 2 · Frameworks & design8 interview questions
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. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally
  5. 4A. Deep Dive — Custom Fragments and Base Classes
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

Spring Data's architecture turns a declared interface into a working repository at start-up. Three parts cooperate:

Spring Data architecture
Figure 1 — Spring Data architecture
PartRoleKey types
Repository interfacesYour contract: base interface + finder methods + optional fragmentsRepository<T, ID>, CrudRepository, JpaRepository, custom fragments
Repository proxyRuntime implementation created per interfaceRepositoryFactorySupport, JpaRepositoryFactoryBean, JDK dynamic proxy
Query generationTurning methods into executable queriesQueryLookupStrategy, PartTree, @Query, named queries
Table 1 — The three architectural parts

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

JAVA
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);
}
ElementResolution
Methods from base interfacesSimpleJpaRepository (base implementation)
Derived finder methodsQuery created from the method name
@Query / named query methodsDeclared JPQL or native SQL
Fragment methodsYour implementation class (…Impl)
Default methodsExecuted directly on the proxy
Table 2 — How each method is implemented

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

A repository call through the proxy
Figure 2 — A repository call through the proxy
Proxy layerResponsibility
Default method invokerRuns Java default methods
Transaction interceptor@Transactional from SimpleJpaRepository (read-only for finders) and your annotations
Exception translationPersistenceException → Spring DataAccessException (via @Repository semantics)
Query executor interceptorExecutes derived/declared queries
Implementation dispatchRoutes base and fragment methods to their target objects
Table 3 — Proxy interceptor chain
Composing a repository from fragments
Figure 3 — Composing a repository from fragments

3.3 Query generation

Query lookup strategy
Figure 4 — Query lookup strategy
Strategy (queryLookupStrategy)Behaviour
CREATE_IF_NOT_FOUND (default)Use declared query (@Query, named query) if present; otherwise derive from the method name
USE_DECLARED_QUERYDeclared queries only; fail otherwise
CREATEAlways derive from the name
Table 4 — Query lookup strategies

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):

JAVA
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):

JAVA
@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 { }
ExtensionScopeUse for
Query methodsOne repositorySimple finders
FragmentSelected repositories (reusable)Complex queries, Criteria/Querydsl, batch operations
Custom base classAll repositoriesCross-cutting operations (getRequired, soft delete helpers)
Default methodsOne repositoryConvenience wrappers around other methods
Table 5 — Extension mechanisms

5APIs / Features / Syntax

API / propertyPurpose
@EnableJpaRepositories (auto-configured by Boot)Scanning, base class, lookup strategy, postfix
@NoRepositoryBeanExclude intermediate interfaces
JpaRepositoryFactoryBean, JpaRepositoryFactoryRepository creation
SimpleJpaRepositoryDefault base implementation
RepositoryQuery, PartTree, QueryLookupStrategyQuery resolution
spring.data.jpa.repositories.bootstrap-modedefault, deferred, lazy initialisation
spring.data.jpa.repositories.enabledToggle repository auto-configuration
Table 6 — Architecture-related APIs

6Practical Examples

6.1 Seeing the proxy

JAVA
@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

TEXT
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

YAML
spring:
  data:
    jpa:
      repositories:
        # repositories initialised in the background after context refresh
        bootstrap-mode: deferred

6.4 Reusable fragment across repositories

JAVA
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

ConcernGuidance
Start-up timeMany repositories and queries parse at start-up; deferred/lazy bootstrap or AOT repositories (Topic 3.1) help
Proxy overheadNegligible compared with database access
TransactionsEach repository call outside a service transaction opens its own transaction
ObservabilityEnable SQL logging/metrics per repository method in tests and staging
Table 7 — Production guidance

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…Repositories packages).
  • Keep complex reads in dedicated query components where derived queries become unreadable.

10Interview Questions & Answers

Q1BasicInternals

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.

Q2BasicConcept

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.

Q3IntermediateConcept

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.

Q4IntermediateConcept

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.

Q5Senior EngineerCode

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.

Q6Senior EngineerTricky

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.

Q7Technical ArchitectDesign

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.

Q8Solution ArchitectDesign

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.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 4

Spring & Spring Boot

Volume I · Topic 1.1

Why Spring Exists

Spring Core, IoC & Dependency Injection

Volume I · Topic 1.2

Spring Framework Architecture

Spring Core, IoC & Dependency Injection

Volume I · Topic 1.3

Spring Modules

Spring Core, IoC & Dependency Injection

Volume II · Topic 1.1

Cross-Cutting Concerns

Spring AOP, Proxies, Events & Transactions

Volume II · Topic 1.2

Why Cross-Cutting Concerns Are Difficult

Spring AOP, Proxies, Events & Transactions

Volume II · Topic 1.3

Aspect-Oriented Programming

Spring AOP, Proxies, Events & Transactions

Volume III · Topic 1.1

Why Spring Boot Exists

Spring Boot, Configuration & Application Lifecycle

Volume III · Topic 1.2

Spring Boot Architecture

Spring Boot, Configuration & Application Lifecycle

Volume III · Topic 1.3

Spring Boot vs Spring Framework

Spring Boot, Configuration & Application Lifecycle

Volume IV · Topic 1.1

MVC

Spring MVC, REST & Web Applications

Volume IV · Topic 1.2

DispatcherServlet

Spring MVC, REST & Web Applications

Volume IV · Topic 1.3

MVC Request Lifecycle

Spring MVC, REST & Web Applications

Volume V · Topic 1.1

Repository Abstraction

Spring Data, JPA & Persistence

Volume V · Topic 1.3

Repository Types

Spring Data, JPA & Persistence

Volume VI · Topic 1.1

Security Architecture

Spring Security, OAuth2 & Identity

Volume VI · Topic 1.2

Security Filter Chain

Spring Security, OAuth2 & Identity

Volume VI · Topic 1.3

Security Context

Spring Security, OAuth2 & Identity

Volume VII · Topic 1.1

Enterprise Integration

Spring Integration, Testing & Cloud-Native Development

Volume VII · Topic 1.2

Integration Patterns

Spring Integration, Testing & Cloud-Native Development

Volume VII · Topic 1.3

Messaging Systems

Spring Integration, Testing & Cloud-Native Development