Spring Data JPA Repositories: Which One to Use and When
Stop guessing which Spring Data JPA repository to extend.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Basic knowledge of Spring Boot and JPA
- ✓Familiarity with dependency injection and annotations
- ✓Understanding of database concepts and SQL
- 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.
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().
- 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.
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.
- 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.
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.
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.
flush() and clear() after every 100 saves reduced runtime from 5 minutes to 30 seconds.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:
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.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.deleteAll()loads all entities into memory first. If you have 1 million records, deleteAll() will try to load them all. UsedeleteAllInBatch()(JpaRepository) or a custom@Modifyingquery.getOne()vsfindById():getOne()returns a proxy and throws EntityNotFoundException when you access a property.findById()returns an Optional. The docs saygetOne()is for when you're sure the entity exists, but in production, assumptions fail. Always usefindById()unless you're setting a reference for a parent-child relationship.- Transaction boundaries matter. Repository methods are transactional by default, but if you call multiple repository methods in a service, you need
@Transactionalon the service method. Otherwise, you'll get multiple transactions and potential LazyInitializationException. - 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.
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.
The Pagination Outage That Brought Down a SaaS Dashboard
- 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.
flush() and clear() in loops for large batches.SELECT * FROM your_table LIMIT 100 OFFSET 0git log --oneline | grep 'extends'| File | Command / Code | Purpose |
|---|---|---|
| RepositoryHierarchyDemo.java | public interface LookupRepository extends CrudRepository | The Repository Hierarchy |
| CrudRepositoryExample.java | public interface CountryCodeRepository extends CrudRepository| CrudRepository | |
| PagingAndSortingExample.java | public interface AuditLogRepository extends PagingAndSortingRepository| PagingAndSortingRepository | |
| JpaRepositoryExample.java | public interface PaymentRepository extends JpaRepository | JpaRepository |
| BatchInsertConfig.java | spring.jpa.properties.hibernate.jdbc.batch_size=20 | What the Official Docs Won't Tell You |
| DecisionFlow.java | public interface CountryCodeRepository extends CrudRepository| Practical Guidelines for Choosing a Repository Type | |
Key takeaways
Interview Questions on This Topic
What is the difference between CrudRepository, PagingAndSortingRepository, and JpaRepository?
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.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Hibernate & JPA. Mark it forged?
5 min read · try the examples if you haven't