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

Repository Abstraction

Repository, CRUD, paging and sorting

7 min readStage 2 · Frameworks & design6 interview questions
Chapter
Chapter 1 — Spring Data Fundamentals
Prerequisites
Volumes I–II; JPA basics (entities, mappings)
You will be able to
Explain the repository abstraction, use CRUD operations correctly, implement paging and sorting end to end, and design repositories around aggregates
Covers
Repository · CRUD · Paging · Sorting
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 — save() and the "Is New?" Decision
  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

A repository is a collection-like abstraction for loading and storing domain objects: callers ask for aggregates by identity or criteria and hand back aggregates to be persisted, without knowing how storage works. Spring Data implements this pattern: you declare an interface extending a Spring Data base interface, and Spring generates the implementation.

The repository abstraction
Figure 1 — The repository abstraction
JAVA
@Entity
@Table(name = "orders")
public class Order {
    @Id private UUID id;
    @Enumerated(EnumType.STRING) private OrderStatus status;
    private BigDecimal total;
    private Instant createdAt;
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderLine> lines = new ArrayList<>();
    // constructors, domain methods
}
 
public interface OrderRepository extends ListCrudRepository<Order, UUID>,
                                         ListPagingAndSortingRepository<Order, UUID> {
    Page<Order> findByStatus(OrderStatus status, Pageable pageable);
}

2Why It Exists

Before Spring Data, every entity needed a DAO class with the same CRUD code and hand-written queries, plus transaction and exception-translation boilerplate. The Repository pattern (Fowler, PoEAA; Evans, DDD) suggested a domain-centric collection abstraction. Spring Data (2011) combined both ideas: a common programming model across stores (JPA, MongoDB, Redis, Elasticsearch, JDBC, R2DBC …), generated implementations, and query derivation from method names — eliminating most DAO code while keeping store-specific power available.

3Core Concepts

3.1 Repository

AspectMeaning
Marker interfaceRepository<T, ID> — domain type and id type; no methods
Domain typeUsually an aggregate root (Order, not OrderLine)
Collection semanticssave adds/updates, findById retrieves, delete removes
Store independenceSame interfaces work with JPA, MongoDB, JDBC… (store-specific ones add features)
DiscoverySpring Boot auto-configures scanning in the application package and below
Table 1 — Repository essentials
Repository per aggregate versus per table
Figure 2 — Repository per aggregate versus per table

3.2 CRUD

CRUD operations out of the box
Figure 3 — CRUD operations out of the box
MethodBehaviour (JPA)Note
save(entity)persist if new, otherwise mergeReturns the managed instance — use the return value
saveAll(iterable)Loop of saveBatching needs JDBC batch settings (Chapter 4)
findById(id)EntityManager.find → Optional<T>Uses persistence context cache
findAllById(ids)IN queryMissing ids are silently absent
existsById(id)Count/exists queryNo entity loading
count()SELECT COUNT(*)Expensive on huge tables
deleteById(id)Loads entity, then removesCascades and callbacks apply
deleteAll()Loads all, deletes one by oneUse deleteAllInBatch (JPA) for bulk deletes
Table 2 — CRUD semantics

New or existing? SimpleJpaRepository.save asks JpaEntityInformation.isNew(entity): by default, an entity is new when its @Id is null (or a primitive 0) or, if it has a @Version attribute, when the version is null. Entities with application-assigned ids (UUIDs set in the constructor) are therefore considered existing and merged — causing an extra SELECT. Fix it with a @Version field or by implementing Persistable<ID>.

3.3 Paging

Paging and sorting flow
Figure 4 — Paging and sorting flow
JAVA
Page<Order> page = orders.findByStatus(OrderStatus.OPEN, PageRequest.of(2, 20));
page.getContent();          // up to 20 orders
page.getTotalElements();    // requires a COUNT query
page.hasNext();
 
Slice<Order> slice = orders.findSliceByStatus(OrderStatus.OPEN, PageRequest.of(2, 20));
slice.hasNext();            // fetches 21 rows to decide — no COUNT query

3.4 Sorting

JAVA
// stable ordering
Sort sort = Sort.by(Sort.Order.desc("createdAt"), Sort.Order.asc("id"));
List<Order> recent = orders.findAll(sort);
 
// Type-safe sort with property references
Sort typed = Sort.sort(Order.class).by(Order::getCreatedAt).descending();

Sorting is translated into ORDER BY on mapped properties (including nested paths such as customer.name, which add joins). Always include a unique tie-breaker (e.g. id) when paging, or rows with equal sort keys may appear on two pages.

4How It Works Internally

1. At start-up, Boot's JpaRepositoriesAutoConfiguration enables repository scanning; each interface becomes a JpaRepositoryFactoryBean bean definition.

2. The factory creates a proxy whose target is SimpleJpaRepository (CRUD, paging) and which also handles query methods (Topic 1.2).

3. CRUD calls delegate to the shared, transaction-bound EntityManager: persist, merge, find, remove, createQuery.

4. Paging: Spring Data builds a JPQL query with ORDER BY from the Sort, sets firstResult = page × size and maxResults = size, and — for Page — derives and executes a count query.

5. All SimpleJpaRepository methods are transactional: reads with readOnly = true, writes with a regular transaction, both joining an existing transaction if present.

4ADeep Dive — save() and the "Is New?" Decision

JAVA
@Entity
public class Customer implements Persistable<UUID> {
    @Id private UUID id = UUID.randomUUID();            // assigned before persisting
    @Transient private boolean isNew = true;
 
    @Override public UUID getId() { return id; }
    @Override public boolean isNew() { return isNew; }
 
    @PostLoad @PostPersist
    void markNotNew() { this.isNew = false; }
}
Id strategysave without Persistable/@VersionConsequence
@GeneratedValue (sequence/identity)id null → persistINSERT (identity: immediate)
Assigned UUID in constructorid set → mergeSELECT by id, then INSERT
Assigned id + @Version Long versionversion null → persistINSERT only
Assigned id + PersistableisNew() decidesINSERT only
Table 3 — How the id strategy affects save()

5APIs / Features / Syntax

APIPurpose
Repository<T, ID>, CrudRepository, ListCrudRepositoryBase interfaces
PagingAndSortingRepository, ListPagingAndSortingRepositoryPaging and sorting
Pageable, PageRequest, Sort, Sort.TypedSortPaging and ordering parameters
Page<T>, Slice<T>, Window<T>Results
Persistable<ID>, @VersionNew-entity detection
spring.data.jpa.repositories.*, @EnableJpaRepositoriesConfiguration
@DataJpaTestRepository slice tests
Table 4 — Repository APIs

6Practical Examples

6.1 Paging from the web layer

JAVA
@GetMapping("/api/orders")
PagedModel<OrderView> list(@RequestParam OrderStatus status,
                           @PageableDefault(size = 20, sort = "createdAt",
                                   direction = Sort.Direction.DESC) Pageable pageable) {
    return new PagedModel<>(orders.findByStatus(status, pageable).map(OrderView::from));
}

6.2 SQL produced for a paged query

SQL
select o1_0.id, o1_0.created_at, o1_0.status, o1_0.total
from orders o1_0 where o1_0.status = ? order by o1_0.created_at desc
offset ? rows fetch first ? rows only;
select count(o1_0.id) from orders o1_0 where o1_0.status = ?;

6.3 Repository test with Testcontainers

JAVA
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTest {
    @Container @ServiceConnection
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:17");
 
    @Autowired OrderRepository orders;
 
    @Test
    void pagesOpenOrders() {
        orders.saveAll(sampleOrders(45));
        Page<Order> page = orders.findByStatus(OrderStatus.OPEN, PageRequest.of(2, 20));
        assertThat(page.getContent()).hasSize(5);
        assertThat(page.getTotalElements()).isEqualTo(45);
    }
}

6.4 Bulk delete without loading entities

JAVA
@Transactional
public void purgeCancelled(List<UUID> ids) {
    // single DELETE ... WHERE id IN (...) — no cascades/callbacks
    orders.deleteAllByIdInBatch(ids);
}

6.5 Sorting safely

JAVA
// Type-safe sort with property paths
Sort sort = Sort.sort(Order.class).by(Order::getCreatedAt).descending()
                .and(Sort.by("id").descending());               // unique tie-breaker
 
// Sorting by a computed expression requires JpaSort.unsafe (never with client input)
Sort byLineCount = JpaSort.unsafe(Sort.Direction.DESC, "size(o.lines)");
Sorting ruleReason
Whitelist client-supplied sort propertiesPrevent unindexed sorts and property-path probing
Add a unique tie-breaker (id)Deterministic paging
Index the sort columns (with filter columns first)Avoid filesort on large tables
Avoid JpaSort.unsafe with user inputIt passes raw expressions into JPQL
Table 5 — Sorting rules

6.6 Collection-like semantics of a repository

Collection operationRepository equivalentNote
add(order)save(order)Insert or merge
get(id)findById(id)Optional — absence is explicit
contains(id)existsById(id)SELECT count or EXISTS query
remove(order)delete(order)Loads and removes managed entity
size()count()Beware on large tables
iterationfindAll(Pageable)Never iterate unpaged on large tables
Table 6 — The repository as an in-memory collection illusion

7Common Problems & Mistakes

  • Repositories for every table instead of aggregates.
  • Ignoring the return value of save (merge returns a different instance).
  • findAll() on large tables — loads everything into memory.
  • Paging without a unique tie-breaker sort.
  • deleteAll() for bulk deletes — N selects and N deletes.
  • Assigned ids without @Version/Persistable — extra SELECT per insert.

8Performance & Production Considerations

ConcernGuidance
CountsPage triggers counts — prefer Slice/Window for large tables
Deep pagesOffset cost grows; keyset scrolling (Chapter 4)
Bulk operationsdeleteAllInBatch, @Modifying queries (bypass entity lifecycle)
MonitoringQuery counts and durations per repository method (Hibernate statistics, traces)
Table 7 — Production guidance

9Architecture & Design Considerations

  • Repositories per aggregate root, with domain-meaningful finders.
  • Expose the minimum (custom base interfaces, Topic 1.3).
  • Keep repositories free of business logic; rules live in the domain and services.
  • Read models (projections, separate query services) for complex reads — CQRS-lite.

10Interview Questions & Answers

Q1Basic

What is a Spring Data repository?

A Spring Data repository is an interface you declare, for example interface OrderRepository extends JpaRepository<Order, Long>, for which Spring generates an implementation at start-up. It provides standard operations such as save, findById, findAll and delete, plus paging and sorting. You can add query methods whose names Spring parses into queries, such as findByCustomerIdAndStatus, or define queries explicitly with @Query. Repositories hide most data-access boilerplate behind a consistent interface across stores such as JPA, MongoDB and Redis. They work best for aggregate persistence and simple queries; complex reporting usually needs dedicated query code.

Q2 · [Basic] · What is the difference between `Page` and `Slice`?
Answer: Both represent one chunk of a larger result. A Page knows the total number of elements and pages, so the user interface can show "page 3 of 42"; to get that, Spring Data runs an extra count query, which can be expensive on large tables. A Slice only knows whether a next slice exists, which it finds by fetching one more row than requested, so no count query is needed. Use Page when users really need totals, and Slice for infinite scrolling or "load more" buttons. For very large data sets, keyset-based scrolling is more efficient than offset paging.
Q3 · [Intermediate] · How does `save()` decide between insert and update?
Answer: SimpleJpaRepository.save asks the entity information whether the entity is new. By default, an entity is new if its identifier is null, or, when it has a @Version attribute, if the version is null; an entity can also decide for itself by implementing Persistable and its isNew method. For a new entity, save calls EntityManager.persist, which schedules an INSERT. Otherwise it calls merge, which loads the existing row if it is not already in the persistence context and copies the state across, resulting in an UPDATE at flush time. Knowing this explains several surprising extra SELECT statements.
Q4Intermediate

Why include a unique column in the sort when paging?

Offset-based paging assumes a stable order. If you sort only by a non-unique column, such as created date or status, rows with equal values have no defined order, and the database may return them in a different order on each query. A row can then appear on two pages or be skipped entirely as the user moves between pages, especially when data changes in between. Adding a unique column, usually the primary key, as the last sort key makes the order deterministic, for example Sort.by("createdAt").descending().and(Sort.by("id")). Keyset paging needs the same deterministic order.

Q5Senior Engineer

Why can assigned UUIDs cause extra queries on save?

When an application assigns a UUID before saving, the identifier is never null, so Spring Data treats the entity as existing and calls merge instead of persist. Merge must find out whether the row exists, so Hibernate issues a SELECT before the INSERT, doubling the statements for every new entity. On bulk inserts this noticeably slows down writes. Fix it by adding a @Version attribute, which is null for new entities and is also useful for optimistic locking, or by implementing Persistable with an isNew flag that is set to false after persisting or loading.

Q6Senior Engineer

Why is deleteAll() slow and what is the alternative?

deleteAll without arguments loads every entity of that type into memory and then deletes them one by one, issuing one DELETE per row, so deleting a large table can take minutes and use a lot of memory. It does this so that JPA life-cycle callbacks and cascades run for each entity. When those are not needed, deleteAllInBatch issues a single DELETE statement, and a @Modifying @Query can delete a filtered set in one statement. Bulk operations bypass the persistence context, so entities already loaded may be stale afterwards; clear the context or run the bulk operation in its own transaction.

Q7Technical Architect

How should repositories relate to aggregates?

In domain-driven design, an aggregate is a cluster of objects that must stay consistent together, with one entity, the aggregate root, as the only entry point. Repositories should follow the same boundary: one repository per aggregate root, such as OrderRepository, and none for internal entities such as OrderLine. All changes to order lines go through the Order, which enforces invariants like a maximum order value, and the whole aggregate is loaded and saved together, usually through cascades. Repositories for child entities let code bypass the root and break invariants, and they blur the consistency boundary that transactions rely on.

Q8Solution Architect

How do you balance repository convenience with read performance at scale?

Use each tool for what it does well. Repositories are ideal for loading and saving aggregates in transactional use cases and for simple lookups. For complex or high-volume reads, such as search screens, dashboards and exports, use projections that select only the needed columns, dedicated query services using JPQL, SQL or jOOQ, or separate read models fed by events. Make paging mandatory for list endpoints, and prefer keyset scrolling for large data sets. Measure query counts and latency in tests and production, and publish guidance so teams know when to step outside the repository abstraction.

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.2

Spring Data Architecture

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