Back to Spring Data Repositories
Q3Question 3 of 4

CrudRepository vs JpaRepository: the differences?

Key Concepts
  • CrudRepository provides basic CRUD: save, findById, findAll (Iterable), delete, count, and existsById.
  • PagingAndSortingRepository extends CrudRepository with findAll(Pageable) and findAll(Sort) for pagination and sorting.
  • JpaRepository extends PagingAndSortingRepository and adds JPA-specific operations: saveAll, flush, saveAndFlush, deleteAllInBatch, and getReferenceById.
  • JpaRepository overrides findAll to return a List instead of an Iterable, which is more convenient.
  • deleteAllInBatch issues a single bulk JPQL DELETE that bypasses cascades and lifecycle callbacks.
  • Expose the narrowest interface that meets the need to communicate intent clearly.

Definition

CrudRepository is basic CRUD, PagingAndSortingRepository adds paging and sorting, and JpaRepository adds JPA batch and flush power.
  • CrudRepository – provides basic create, read, update, and delete operations: save, findById, findAll (returns Iterable), delete, count, and existsById.
  • PagingAndSortingRepository – extends CrudRepository with findAll(Pageable) returning a Page and findAll(Sort) returning sorted results.
  • JpaRepository – extends PagingAndSortingRepository and adds JPA-specific batch and flush operations: saveAll, flush, saveAndFlush, deleteAllInBatch, getReferenceById, and overrides findAll to return a List.
  • Inheritance chain – CrudRepository < PagingAndSortingRepository < JpaRepository, each level adding more capability.
  • Interface segregation – expose the narrowest interface that meets the need, so the contract communicates intent.

Think of three tiers of bank-teller training at National Bank of India. CrudRepository is the basic teller who can open, read, update, and close accounts one at a time. PagingAndSortingRepository is a teller who can also hand you results twenty at a time, sorted by date. JpaRepository is the senior teller who can additionally do bulk operations efficiently: save a whole stack at once, flush immediately to the vault, or batch-delete, which is exactly what National Bank of India needs for heavy batch processing.

Understanding the Concepts

What is CrudRepository?

CrudRepository is the base interface from Spring Data Commons that provides the essential CRUD operations. It works with any Spring Data module (JPA, MongoDB, Redis), not just JPA. The key methods are save, findById, findAll, deleteById, delete, count, and existsById. Its findAll returns an Iterable, not a List.

JAVA
package com.bank.repository;

import org.springframework.data.repository.CrudRepository;
import com.bank.model.Account;

public interface AccountCrudRepository extends CrudRepository<Account, String> {
    // Inherits: save, findById, findAll (Iterable), delete, count, existsById
}
JAVA
package com.bank.service;

import com.bank.repository.AccountCrudRepository;
import com.bank.model.Account;
import java.math.BigDecimal;
import java.util.Optional;

public class AccountLookupService {

    private final AccountCrudRepository repository;

    public AccountLookupService(AccountCrudRepository repository) {
        this.repository = repository;
    }

    public BigDecimal getBalance(String accountId) {
        Optional<Account> account = repository.findById(accountId);
        return account.map(Account::getBalance)
                .orElseThrow(() -> new IllegalArgumentException("Account not found: " + accountId));
    }

    public boolean accountExists(String accountId) {
        return repository.existsById(accountId);
    }

    public long totalAccounts() {
        return repository.count();
    }
}
TEXT
Expected behaviour:
- findById("SAV2001") -> Optional<Account> with Rajesh's account
- existsById("SAV2001") -> true (SELECT 1 WHERE ... EXISTS)
- count() -> total number of accounts in the table

What does PagingAndSortingRepository add?

PagingAndSortingRepository extends CrudRepository and adds two methods: findAll(Pageable) that returns a Page, and findAll(Sort) that returns sorted results. This is the abstraction for paginated and sorted data access, essential for any screen that shows large result sets.

JAVA
package com.bank.repository;

import org.springframework.data.repository.PagingAndSortingRepository;
import com.bank.model.Account;

public interface AccountPagingRepository extends PagingAndSortingRepository<Account, String> {
    // Inherits CrudRepository methods PLUS:
    // Page<Account> findAll(Pageable pageable)
    // Iterable<Account> findAll(Sort sort)
}
JAVA
package com.bank.service;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import com.bank.repository.AccountPagingRepository;
import com.bank.model.Account;

public class StatementListService {

    private final AccountPagingRepository repository;

    public StatementListService(AccountPagingRepository repository) {
        this.repository = repository;
    }

    public Page<Account> getAccountsPage(int page, int size) {
        return repository.findAll(
                PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "balance")));
    }
}
TEXT
Expected SQL: SELECT * FROM accounts ORDER BY balance DESC LIMIT 20 OFFSET 0
Page object contains: content (List<Account>), totalElements, totalPages, hasNext, etc.

What does JpaRepository add on top?

JpaRepository extends PagingAndSortingRepository and layers on JPA-specific methods. saveAll persists a collection of entities efficiently. flush forces pending changes to the database immediately. saveAndFlush saves and flushes in one call. deleteAllInBatch issues a single bulk DELETE instead of per-row deletes. getReferenceById returns a lazy proxy without hitting the database.

JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.Account;

public interface AccountRepository extends JpaRepository<Account, String> {
    // Inherits ALL CrudRepository + PagingAndSortingRepository methods PLUS:
    // List<Account> saveAll(Iterable<Account> entities)
    // void flush()
    // Account saveAndFlush(Account entity)
    // void deleteAllInBatch()
    // Account getReferenceById(String id)  -- lazy proxy
    // List<Account> findAll()  -- returns List, not Iterable
}
JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bank.repository.AccountRepository;
import com.bank.model.Account;
import java.util.List;

@Service
public class BatchImportService {

    private final AccountRepository accountRepository;

    public BatchImportService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional
    public void importAccounts(List<Account> accounts) {
        // saveAll: persists the entire list, batching if hibernate.jdbc.batch_size is set
        accountRepository.saveAll(accounts);
        // flush: forces all pending INSERTs to the database now
        accountRepository.flush();
    }
}
TEXT
Expected behaviour with batch_size=50:
- saveAll groups 100,000 accounts into batches of 50 INSERTs each
- flush pushes all batches to the database
- Far faster than saving one by one with CrudRepository.save()

How do the three interfaces compare?

| Feature | CrudRepository | PagingAndSortingRepository | JpaRepository |
|---|---|---|---|
| save / findById / delete | Yes | Yes | Yes |
| findAll return type | Iterable | Iterable | List |
| findAll(Pageable) | No | Yes | Yes |
| findAll(Sort) | No | Yes | Yes |
| saveAll (batch) | No | No | Yes |
| flush / saveAndFlush | No | No | Yes |
| deleteAllInBatch | No | No | Yes |
| getReferenceById | No | No | Yes |
| Module | Spring Data Commons | Spring Data Commons | Spring Data JPA |

JAVA
// CrudRepository: Iterable return
package com.bank.demo;

import com.bank.model.Account;

public class IterableVsList {
    public void demonstrate() {
        // CrudRepository.findAll() returns Iterable<Account>
        // You must convert to List manually:
        // List<Account> accounts = StreamSupport.stream(repo.findAll().spliterator(), false)
        //         .collect(Collectors.toList());

        // JpaRepository.findAll() returns List<Account> directly
        // List<Account> accounts = repo.findAll();  // much more convenient
    }
}

What are the rules for choosing which interface?

The guiding principle is interface segregation: expose the narrowest interface that meets the need. A simple lookup service extends CrudRepository to signal that it only does basic operations. A paginated statement screen extends PagingAndSortingRepository. A batch job that imports or purges large volumes extends JpaRepository. In practice, many teams default to JpaRepository because List returns and batch operations are commonly useful.

JAVA
// Simple lookup: CrudRepository signals minimal operations
package com.bank.repository;

import org.springframework.data.repository.CrudRepository;
import com.bank.model.Customer;

public interface CustomerLookupRepository extends CrudRepository<Customer, String> {
}

// Paginated listing: PagingAndSortingRepository signals page/sort support
import org.springframework.data.repository.PagingAndSortingRepository;
import com.bank.model.Transaction;

public interface TransactionListRepository extends PagingAndSortingRepository<Transaction, Long> {
}

// Batch processing: JpaRepository signals flush/batch power
import org.springframework.data.jpa.repository.JpaRepository;
import com.bank.model.Account;

public interface AccountBatchRepository extends JpaRepository<Account, String> {
}

What are common mistakes?

The most common mistake is using saveAndFlush inside a loop, which defeats batching entirely. A second mistake is using deleteAllInBatch and expecting cascade and lifecycle callbacks to fire, which they do not because it issues a single bulk JPQL DELETE.

JAVA
// WRONG: saveAndFlush in a loop defeats batching
package com.bank.service;

import com.bank.repository.AccountRepository;
import com.bank.model.Account;
import java.util.List;

public class BadBatchService {

    private final AccountRepository repo;

    public BadBatchService(AccountRepository repo) {
        this.repo = repo;
    }

    public void importBadly(List<Account> accounts) {
        for (Account a : accounts) {
            repo.saveAndFlush(a);  // WRONG: one INSERT + one flush per row
        }
    }
}
JAVA
// CORRECT: saveAll + single flush
package com.bank.service;

import com.bank.repository.AccountRepository;
import com.bank.model.Account;
import java.util.List;

public class GoodBatchService {

    private final AccountRepository repo;

    public GoodBatchService(AccountRepository repo) {
        this.repo = repo;
    }

    public void importCorrectly(List<Account> accounts) {
        repo.saveAll(accounts);  // batch INSERTs
        repo.flush();            // one flush at the end
    }
}
TEXT
Performance difference for 10,000 accounts:
- saveAndFlush per row: ~45 seconds (10,000 round-trips)
- saveAll + flush:      ~2 seconds  (200 batched round-trips with batch_size=50)

What about getReferenceById vs findById?

getReferenceById returns a lazy proxy without hitting the database. If the entity does not exist, the proxy throws EntityNotFoundException lazily when you access a property. findById executes a SELECT immediately and returns an Optional. Use findById when you need to confirm existence; use getReferenceById when you only need a reference to set a foreign key.

JAVA
package com.bank.service;

import com.bank.repository.AccountRepository;
import com.bank.model.Account;
import com.bank.model.Trade;

public class TradeService {

    private final AccountRepository accountRepository;

    public TradeService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    public Trade createTrade(String accountId, Trade trade) {
        // getReferenceById: no SELECT, just a proxy to set the FK
        Account accountRef = accountRepository.getReferenceById(accountId);
        trade.setAccount(accountRef);
        // The proxy's id is enough to populate the FK column on trade
        return trade;
    }
}

What should a senior engineer know?

A senior engineer knows that deleteAllInBatch bypasses cascade and lifecycle callbacks because it is a bulk JPQL DELETE, so it should only be used where @PreRemove logic and cascades are not needed. They know that saveAndFlush is misused when called in a loop because it defeats batching. They know that getReferenceById throws EntityNotFoundException lazily, so when existence must be confirmed, findById is the safer choice. They also know that JpaRepository's saveAll works best when hibernate.jdbc.batch_size is configured in application properties.

YAML
# application.yml: enable JDBC batching for saveAll
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true

Visual Overview

TEXT
  Repository Inheritance Chain
  ============================================

  +-----------------------+
  |   CrudRepository      |  <- Spring Data Commons
  |-----------------------|
  | save()                |
  | findById()            |
  | findAll() -> Iterable |
  | delete()              |
  | count()               |
  | existsById()          |
  +----------+------------+
             |
             v
  +-----------------------------+
  | PagingAndSortingRepository  |  <- Spring Data Commons
  |-----------------------------|
  | findAll(Pageable) -> Page   |
  | findAll(Sort) -> Iterable   |
  +----------+------------------+
             |
             v
  +-----------------------------+
  |      JpaRepository          |  <- Spring Data JPA
  |-----------------------------|
  | saveAll()                   |
  | flush()                     |
  | saveAndFlush()              |
  | deleteAllInBatch()          |
  | getReferenceById()          |
  | findAll() -> List (override)|
  +-----------------------------+

Key Takeaway

These three interfaces form an inheritance chain of increasing capability at National Bank of India. CrudRepository provides the essentials: save, findById returning an Optional, findAll returning an Iterable, delete, count, and existsById. PagingAndSortingRepository extends it and adds findAll with a Pageable and findAll with a Sort, which is where Rajesh gets the page-size-sort abstraction for listing thousands of accounts. JpaRepository extends that further and layers on JPA-specific power: saveAll for bulk persistence, flush and saveAndFlush for explicit flush control, deleteAllInBatch for a single bulk delete, and it overrides findAll to return a List.

The guiding principle Pankaj enforces is interface segregation: expose the narrowest interface that meets the need. A simple customer-lookup service extends CrudRepository so a reviewer immediately knows it does only basic operations. A paginated statement screen extends PagingAndSortingRepository. A nightly batch job that imports a hundred thousand accounts extends JpaRepository because it needs saveAll, flush, and deleteAllInBatch.

The senior cautions are that deleteAllInBatch issues a bulk JPQL DELETE that bypasses cascades and @PreRemove callbacks, that saveAndFlush is frequently misused inside loops in a way that destroys batching, and that getReferenceById throws its not-found exception lazily so when you need to confirm existence you should use findById instead.

Remember one sentence: CrudRepository gives basic CRUD, PagingAndSortingRepository adds pagination, and JpaRepository adds batch, flush, and List returns for JPA-specific power.

---

Share
Use ← → arrow keys to navigate