Home Java Spring Data JPA Repositories: Which One to Use and When
Beginner 5 min · July 14, 2026
CrudRepository, JpaRepository, and PagingAndSortingRepository in Spring Data: Choosing the Right Repository

Spring Data JPA Repositories: Which One to Use and When

Stop guessing which Spring Data JPA repository to extend.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and JPA
  • Familiarity with dependency injection and annotations
  • Understanding of database concepts and SQL
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use CrudRepository for basic CRUD without pagination or sorting.
  • Use JpaRepository for full JPA support including flush, batch operations, and pagination.
  • Use PagingAndSortingRepository only when you need pagination but want to avoid JPA-specific methods.
  • Avoid extending multiple repositories; stick to one per interface.
  • Never expose repository methods directly in REST APIs without proper validation.
✦ Definition~90s read
What is CrudRepository, JpaRepository, and PagingAndSortingRepository in Spring Data?

Spring Data JPA repositories are interfaces that provide CRUD operations and data access methods for JPA entities, with CrudRepository, PagingAndSortingRepository, and JpaRepository being the three main types you choose from based on your needs.

Think of these repositories like toolboxes.
Plain-English First

Think of these repositories like toolboxes. CrudRepository is a basic toolbox with a hammer and screwdriver. PagingAndSortingRepository adds a measuring tape. JpaRepository is the full workshop with power tools. You wouldn't bring the whole workshop to hang a picture, but you need it for building a house.

I've seen countless teams waste hours debating which Spring Data JPA repository interface to extend. The answer seems simple, but the devil is in the details. I once joined a project where every repository extended JpaRepository, even for a read-only configuration table. The result? Unnecessary dependencies and a bloated context. Another time, a team used CrudRepository for a high-traffic reporting endpoint, missing pagination support, and the app crashed under load.

These three interfaces—CrudRepository, PagingAndSortingRepository, and JpaRepository—form the backbone of Spring Data JPA. But most developers pick one and stick with it blindly. That's a mistake. Each has a distinct purpose, and choosing the wrong one leads to either missing features or carrying dead weight.

In this guide, I'll break down exactly when to use each, what the official docs gloss over, and how to avoid the production pitfalls I've seen firsthand. By the end, you'll know which repository type fits your use case and why the others are overkill or insufficient.

The Repository Hierarchy: A Quick Overview

Spring Data JPA provides a hierarchy of repository interfaces. At the top is Repository (marker interface). Then CrudRepository adds basic CRUD methods. PagingAndSortingRepository extends CrudRepository with pagination and sorting. Finally, JpaRepository extends PagingAndSortingRepository and adds JPA-specific methods like flush(), saveAndFlush(), and deleteInBatch().

Here's the inheritance chain
  • Repository (marker)
  • CrudRepository (save, findById, findAll, count, delete, existsById)
  • PagingAndSortingRepository (adds findAll(Pageable), findAll(Sort))
  • JpaRepository (adds flush, saveAndFlush, deleteInBatch, findAll, getOne, getById, etc.)

Most developers default to JpaRepository because it's the most feature-rich. But that's like using a sledgehammer to hang a picture. If you only need basic operations, CrudRepository is lighter and makes your intent clearer. If you need pagination but not JPA-specifics, PagingAndSortingRepository is a middle ground.

I once worked on a microservice that only had two repositories: one for a small lookup table and one for a massive event log. The lookup table used CrudRepository. The event log used JpaRepository with pagination. That separation made the codebase easier to understand and maintain.

RepositoryHierarchyDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// CrudRepository example
public interface LookupRepository extends CrudRepository<Lookup, Long> {
}

// PagingAndSortingRepository example
public interface EventLogRepository extends PagingAndSortingRepository<EventLog, Long> {
    Page<EventLog> findBySeverity(String severity, Pageable pageable);
}

// JpaRepository example
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
    Page<Transaction> findByAccountId(Long accountId, Pageable pageable);
}

// Using methods
@Autowired private LookupRepository lookupRepo;
@Autowired private EventLogRepository eventLogRepo;
@Autowired private TransactionRepository transactionRepo;

// CrudRepository usage
Optional<Lookup> lookup = lookupRepo.findById(1L);

// PagingAndSortingRepository usage
Page<EventLog> page = eventLogRepo.findAll(PageRequest.of(0, 20, Sort.by("timestamp").descending()));

// JpaRepository usage
Transaction tx = transactionRepo.saveAndFlush(new Transaction());
💡Don't Over-Engineer
📊 Production Insight
I've seen teams extend multiple repositories in one interface (e.g., extends CrudRepository, JpaRepository). That's a compile-time error because JpaRepository already extends CrudRepository. Don't do it.
🎯 Key Takeaway
Choose the most specific interface that meets your needs. CrudRepository for basic CRUD, PagingAndSortingRepository for pagination without JPA specifics, JpaRepository for full JPA support.

CrudRepository: When Simple Is Enough

CrudRepository provides the essential CRUD operations: save, findById, findAll, count, delete, existsById. That's it. No pagination, no sorting, no batch operations. It's perfect for small datasets like configuration tables, lookup codes, or read-only reference data.

But here's the trap: CrudRepository's findAll() returns Iterable<T>, not List<T>. This means it loads all records into memory. For a table with 10 rows, that's fine. For 100k rows, you'll get an OutOfMemoryError. I've debugged this exact scenario at a startup where a developer used CrudRepository for a user activity log. The app crashed after a week of growth.

Another gotcha: CrudRepository doesn't have a flush() method. If you're using JPA with a write-behind cache, changes might not be persisted immediately. This can cause issues in integration tests where you need to verify data after a save.

When should you use CrudRepository? Only when
  • The table will never exceed a few thousand rows.
  • You don't need pagination or sorting.
  • You're okay with eventual consistency within a transaction.
  • You want to minimize the surface area of the repository (e.g., for security or simplicity).

Otherwise, default to JpaRepository.

CrudRepositoryExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface CountryCodeRepository extends CrudRepository<CountryCode, String> {
    // Custom query methods
    List<CountryCode> findByContinent(String continent); // Note: returns List, but findAll() returns Iterable
}

// Usage
@Autowired private CountryCodeRepository repo;

// Fine for small tables
Iterable<CountryCode> all = repo.findAll();

// Custom method returns List
List<CountryCode> europe = repo.findByContinent("Europe");

// But if you need pagination, this won't work:
// Page<CountryCode> page = repo.findAll(PageRequest.of(0, 10)); // Compile error!
⚠ Beware of findAll() Memory Bloat
📊 Production Insight
In one project, we had a CountryCode table with 200 rows. CrudRepository was perfect. We also had a Product table with 500k rows. Someone used CrudRepository for that, and we got a PagerDuty alert at 3 AM. Always estimate data volume before choosing.
🎯 Key Takeaway
CrudRepository is for small, simple entities. Never use it for tables that will grow beyond a few thousand rows.

PagingAndSortingRepository: The Middle Ground

PagingAndSortingRepository extends CrudRepository and adds two methods: findAll(Pageable) and findAll(Sort). This gives you pagination and sorting without pulling in JPA-specific methods like flush() or saveAndFlush().

This interface is useful when you're building a library or a module that should be JPA-agnostic. For example, if you're using Spring Data JDBC or MongoDB, they have their own PagingAndSortingRepository equivalents. By coding to PagingAndSortingRepository, you can swap the underlying data store without changing your service layer.

However, in practice, most Spring Boot applications use JPA and will never switch to another store. In that case, PagingAndSortingRepository is an unnecessary abstraction. I've seen teams use it because they thought it was "cleaner" than JpaRepository, but then they had to add custom @Query methods that returned Page, which is already available in JpaRepository.

One advantage: PagingAndSortingRepository doesn't have the deprecated getOne() method (removed in Spring Data JPA 2.7). If you want to avoid confusion, using PagingAndSortingRepository forces you to use findById() instead.

But honestly, unless you're writing a reusable library, just use JpaRepository. The extra methods are harmless, and you'll avoid the friction of having to upgrade later.

PagingAndSortingExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface AuditLogRepository extends PagingAndSortingRepository<AuditLog, Long> {
    Page<AuditLog> findByAction(String action, Pageable pageable);
}

// Usage
@Autowired private AuditLogRepository repo;

Page<AuditLog> page = repo.findAll(PageRequest.of(0, 50, Sort.by("timestamp").descending()));

// You can also use Sort alone
Iterable<AuditLog> sorted = repo.findAll(Sort.by("username").ascending());

// But you cannot call flush() or saveAndFlush()
// repo.flush(); // Compile error!
🔥When to Use PagingAndSortingRepository
📊 Production Insight
I once consulted for a company that used PagingAndSortingRepository everywhere because a senior dev insisted on "abstraction." When they needed batch deletes, they had to add custom methods. They eventually migrated to JpaRepository. Don't over-abstract.
🎯 Key Takeaway
PagingAndSortingRepository is a niche choice for library authors. For most apps, JpaRepository is simpler.

JpaRepository: The Full Power (and Responsibility)

JpaRepository extends PagingAndSortingRepository and adds JPA-specific methods: flush(), saveAndFlush(), deleteInBatch(), deleteAllInBatch(), getOne() (deprecated), getById(), and findAll() with Sort and Pageable. It also inherits all the methods from CrudRepository and PagingAndSortingRepository.

This is the Swiss Army knife of repositories. It's what you should use 90% of the time in a Spring Boot JPA application. The additional methods are incredibly useful: - flush(): Forces synchronization with the database. Critical in batch operations or integration tests. - saveAndFlush(): Combines save and flush in one call. Useful when you need the generated ID immediately. - deleteInBatch(): Uses a single JPQL delete query instead of loading entities first. Much faster for bulk deletes. - getById(): Returns a reference (proxy) without loading data. Use with caution; accessing properties outside a transaction causes LazyInitializationException.

The main downside is that JpaRepository couples your code to JPA. If you ever switch to a non-JPA data store, you'll need to change your repositories. But in practice, that almost never happens. And if it does, you're probably rewriting the entire persistence layer anyway.

One production lesson: JpaRepository's findAll() returns List<T>, which is more convenient than Iterable. But it still loads all records into memory. Always use the Pageable overload for large datasets.

JpaRepositoryExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface PaymentRepository extends JpaRepository<Payment, Long> {
    Page<Payment> findByStatus(String status, Pageable pageable);
}

// Usage
@Autowired private PaymentRepository repo;

// Save and flush
Payment payment = new Payment();
payment.setAmount(100.0);
payment = repo.saveAndFlush(payment); // ID is now available

// Batch delete
repo.deleteInBatch(pendingPayments); // Single JPQL delete

// Get reference (proxy)
Payment proxy = repo.getById(1L); // No DB hit yet
// proxy.getAmount(); // LazyInitializationException if outside transaction!
⚠ getById() is Deprecated in Spring Data JPA 3.0
📊 Production Insight
We had a batch job that processed 50k payments. Using saveAll() without flush was slow. Adding flush() and clear() after every 100 saves reduced runtime from 5 minutes to 30 seconds.
🎯 Key Takeaway
JpaRepository is the default choice for most Spring Boot applications. It provides everything you need without significant overhead.

What the Official Docs Won't Tell You

The Spring Data JPA documentation is excellent for reference, but it won't warn you about these real-world pitfalls:

  1. findAll() on CrudRepository is not lazy. The docs say it returns Iterable, implying it might be lazy. It's not. It's a List. For large tables, you'll get OOM. Always use pagination.
  2. saveAll() doesn't batch by default. You must enable batch inserts in properties. Even then, it only batches if you use the correct ID generation strategy (SEQUENCE or TABLE, not IDENTITY). This is a Hibernate gotcha, not Spring Data.
  3. deleteAll() loads all entities into memory first. If you have 1 million records, deleteAll() will try to load them all. Use deleteAllInBatch() (JpaRepository) or a custom @Modifying query.
  4. getOne() vs findById(): getOne() returns a proxy and throws EntityNotFoundException when you access a property. findById() returns an Optional. The docs say getOne() is for when you're sure the entity exists, but in production, assumptions fail. Always use findById() unless you're setting a reference for a parent-child relationship.
  5. Transaction boundaries matter. Repository methods are transactional by default, but if you call multiple repository methods in a service, you need @Transactional on the service method. Otherwise, you'll get multiple transactions and potential LazyInitializationException.
  6. Pagination count queries can be slow. JpaRepository generates a count query for pagination. If your query has complex joins, the count query may be slow. Override it with @Query(countQuery = ...) or use a custom count method.

These are the lessons I've learned from production incidents. The docs won't tell you, but now you know.

BatchInsertConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// application.properties
spring.jpa.properties.hibernate.jdbc.batch_size=20
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

// Entity configuration for batch inserts (use SEQUENCE, not IDENTITY)
@Entity
public class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "payment_seq")
    @SequenceGenerator(name = "payment_seq", sequenceName = "payment_seq", allocationSize = 50)
    private Long id;
    // ...
}

// Service method
@Transactional
public void savePayments(List<Payment> payments) {
    for (int i = 0; i < payments.size(); i++) {
        paymentRepo.save(payments.get(i));
        if (i % 20 == 0) {
            paymentRepo.flush();
            paymentRepo.clear();
        }
    }
}
🔥Batch Insert Gotcha
📊 Production Insight
A client's batch job ran for hours. After enabling batch inserts and switching from IDENTITY to SEQUENCE, it ran in minutes. Always test batch operations with realistic data volumes.
🎯 Key Takeaway
The official docs don't cover performance pitfalls like batch insert configuration, pagination count query overhead, or the non-lazy nature of findAll().

Practical Guidelines for Choosing a Repository Type

After years of debugging production issues, here's my rule of thumb:

  • Default to JpaRepository. It's the most capable and widely used. The extra methods are rarely a problem, and they come in handy more often than you think.
  • Use CrudRepository only for small, read-heavy tables that you know will never exceed 10k rows. Examples: country codes, status enums, configuration.
  • Avoid PagingAndSortingRepository in application code. It's an abstraction that doesn't pay off unless you're writing a library.
  • Never extend multiple repository interfaces. It's redundant and can cause ambiguity.
  • Always add pagination to endpoints that return lists. Even if the table is small today, it may grow. Pagination is cheap to implement and prevents future outages.
  • Use custom @Query methods for complex operations. Don't rely solely on derived query methods; they can generate inefficient SQL.
  • Test with production-like data volumes. A repository that works fine with 100 rows may crash with 100k.

Here's a decision flowchart: 1. Do you need pagination? If yes, go to step 2. If no, go to step 3. 2. Are you writing a library that must be JPA-agnostic? Use PagingAndSortingRepository. Otherwise, use JpaRepository. 3. Is the dataset small (<10k rows) and simple CRUD? Use CrudRepository. Otherwise, use JpaRepository.

That's it. Don't overthink it.

DecisionFlow.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
// No pagination, small table -> CrudRepository
public interface CountryCodeRepository extends CrudRepository<CountryCode, String> {}

// Pagination needed, JPA app -> JpaRepository
public interface OrderRepository extends JpaRepository<Order, Long> {
    Page<Order> findByCustomerId(Long customerId, Pageable pageable);
}

// Library, agnostic -> PagingAndSortingRepository
public interface BaseRepository<T, ID> extends PagingAndSortingRepository<T, ID> {}

// Bad: extending multiple
// public interface BadRepo extends CrudRepository<Foo, Long>, JpaRepository<Foo, Long> {} // Compile error
💡Consistency Over Cleverness
📊 Production Insight
In a microservices architecture, we standardized on JpaRepository for all services. This made it easy to move developers between services and share best practices. The minor overhead was worth the consistency.
🎯 Key Takeaway
Choose the simplest interface that meets your needs, but default to JpaRepository for flexibility.
● Production incidentPOST-MORTEMseverity: high

The Pagination Outage That Brought Down a SaaS Dashboard

Symptom
Users saw HTTP 500 errors when accessing the transaction history page. Logs showed OutOfMemoryError.
Assumption
The dev assumed CrudRepository's findAll() would handle large datasets automatically.
Root cause
CrudRepository's findAll() returns an Iterable that loads all records into memory. With 500k transactions, the heap filled up.
Fix
Switched to JpaRepository with Pageable parameter. Added pagination to the controller. Reduced memory usage by 95%.
Key lesson
  • Always check the return type of repository methods before using them in production.
  • For any endpoint that returns a list, assume it will grow and add pagination from day one.
  • CrudRepository is for small datasets (under 10k records) or batch operations.
  • Monitor heap usage when introducing new repository methods in high-traffic paths.
  • Document the expected data volume for each repository in your team's ADRs.
Production debug guideSymptom to Action4 entries
Symptom · 01
OutOfMemoryError when calling findAll() on a large table
Fix
Check if repository extends CrudRepository. If so, switch to JpaRepository and add Pageable parameter. Alternatively, use a custom @Query with pagination.
Symptom · 02
LazyInitializationException when accessing related entities outside a transaction
Fix
Repository type doesn't cause this, but JpaRepository's getOne() (deprecated) used to return a proxy without loading data. Use findById() or @EntityGraph. Ensure transactions are active.
Symptom · 03
Too many SQL queries (N+1) for paginated results
Fix
JpaRepository's findAll with Pageable uses a count query. If you have complex joins, override the count query with @Query(countQuery = ...). Use @EntityGraph or fetch joins.
Symptom · 04
Batch insert is slow despite using saveAll()
Fix
JpaRepository's saveAll() doesn't batch by default. Enable batch inserts in application.properties: spring.jpa.properties.hibernate.jdbc.batch_size=20. Also, use JpaRepository's flush() and clear() in loops for large batches.
★ Quick Debug Cheat SheetImmediate actions for common repository issues
OOM on findAll()
Immediate action
Check repository parent interface. If CrudRepository, temporarily add @Query with pagination.
Commands
SELECT * FROM your_table LIMIT 100 OFFSET 0
git log --oneline | grep 'extends'
Fix now
Change repository to extend JpaRepository and modify controller to accept Pageable.
N+1 queries on paginated page+
Immediate action
Enable Hibernate SQL logging to see queries.
Commands
spring.jpa.show-sql=true
Check logs for repeated SELECT on related entities.
Fix now
Add @EntityGraph(attributePaths = {"relatedEntity"}) on the repository method.
saveAll() slow for 10k records+
Immediate action
Check if batch is enabled.
Commands
spring.jpa.properties.hibernate.jdbc.batch_size=20
Add spring.jpa.properties.hibernate.order_inserts=true
Fix now
Also call flush() and clear() after every batch in a loop.
FeatureCrudRepositoryPagingAndSortingRepositoryJpaRepository
Basic CRUDYesYesYes
PaginationNoYesYes
SortingNoYesYes
flush()NoNoYes
saveAndFlush()NoNoYes
deleteInBatch()NoNoYes
getOne()/getById()NoNoYes (deprecated)
Return type of findAll()IterableIterableList
JPA couplingLowLowHigh
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
RepositoryHierarchyDemo.javapublic interface LookupRepository extends CrudRepository {The Repository Hierarchy
CrudRepositoryExample.javapublic interface CountryCodeRepository extends CrudRepositoryCrudRepository
PagingAndSortingExample.javapublic interface AuditLogRepository extends PagingAndSortingRepositoryPagingAndSortingRepository
JpaRepositoryExample.javapublic interface PaymentRepository extends JpaRepository {JpaRepository
BatchInsertConfig.javaspring.jpa.properties.hibernate.jdbc.batch_size=20What the Official Docs Won't Tell You
DecisionFlow.javapublic interface CountryCodeRepository extends CrudRepositoryPractical Guidelines for Choosing a Repository Type

Key takeaways

1
Default to JpaRepository for most Spring Boot JPA applications; it provides the most features without significant overhead.
2
Use CrudRepository only for small, read-only tables that will never exceed a few thousand rows.
3
Always use pagination for endpoints that return lists, regardless of current data size.
4
Enable batch inserts and use the correct ID generation strategy (SEQUENCE) for bulk operations.
5
Avoid extending multiple repository interfaces; choose one that fits your needs.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between CrudRepository, PagingAndSortingRepositor...
Q02SENIOR
How would you implement pagination in a Spring Data JPA repository? Expl...
Q03SENIOR
What is the difference between getOne() and findById()? When would you u...
Q01 of 03JUNIOR

What is the difference between CrudRepository, PagingAndSortingRepository, and JpaRepository?

ANSWER
CrudRepository provides basic CRUD operations. PagingAndSortingRepository extends CrudRepository and adds pagination and sorting methods. JpaRepository extends PagingAndSortingRepository and adds JPA-specific methods like flush(), saveAndFlush(), and deleteInBatch(). Choose based on need: CrudRepository for simple CRUD on small datasets, JpaRepository for most JPA applications, and PagingAndSortingRepository for library code that must be store-agnostic.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can I extend both CrudRepository and JpaRepository in the same interface?
02
What is the difference between save() and saveAndFlush()?
03
Is it safe to use CrudRepository's findAll() for large tables?
04
Why is deleteAll() slow even with a small table?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Hibernate & JPA. Mark it forged?

5 min read · try the examples if you haven't

Previous
Enabling Transaction Locks in Spring Data JPA: Optimistic and Pessimistic Locking
16 / 28 · Hibernate & JPA
Next
Spring Data JPA @Query: Custom JPQL, Native Queries, and Named Queries