Definition
- CrudRepository – basic CRUD:
save(),findById(),findAll()(returnsIterable),deleteById(),count(),existsById(). Technology-agnostic (works with JPA, MongoDB, Redis). - JpaRepository – extends CrudRepository through
ListCrudRepositoryandPagingAndSortingRepository. Adds pagination (findAll(Pageable)), sorting (findAll(Sort)),flush(),saveAndFlush(),deleteAllInBatch(), andgetReferenceById(). - Return types – CrudRepository returns
Iterable; JpaRepository returnsList– ready to use without conversion. - deleteAllInBatch() – executes one
DELETE FROM tableSQL statement.deleteAll()loads all entities first, then deletes each one individually. - Hierarchy –
Repository→CrudRepository→ListCrudRepository→PagingAndSortingRepository→JpaRepository.
CrudRepository is like a basic bank counter at National Bank of India – deposit (save), withdraw (delete), check balance (findById), list transactions (findAll). JpaRepository is the premium counter – everything basic plus: sort transactions by date, paginate (show page 5), flush immediately (saveAndFlush), bulk delete 10,000 old records in one shot (deleteAllInBatch), and lazy references for efficient FK writes.
Understanding the Concepts
What is CrudRepository?
CrudRepository is the basic Spring Data interface that provides standard CRUD operations. It is technology-agnostic: the same interface works with JPA, MongoDB, Redis, and other Spring Data modules.
package com.bank.repository;
import com.bank.model.Account;
import org.springframework.data.repository.CrudRepository;
public interface AccountCrudRepo extends CrudRepository<Account, String> {
// Inherited methods:
// Account save(Account entity)
// Iterable<Account> saveAll(Iterable<Account> entities)
// Optional<Account> findById(String id)
// Iterable<Account> findAll() -- returns Iterable, not List!
// Iterable<Account> findAllById(Iterable<String> ids)
// long count()
// boolean existsById(String id)
// void deleteById(String id)
// void delete(Account entity)
// void deleteAll()
}
What is JpaRepository?
JpaRepository extends CrudRepository (through several intermediate interfaces) and adds JPA-specific features: pagination, sorting, flush control, batch operations, and lazy references.
package com.bank.repository;
import com.bank.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AccountRepository extends JpaRepository<Account, String> {
// All CrudRepository methods PLUS:
// List<Account> findAll() -- returns List, not Iterable!
// List<Account> findAll(Sort sort)
// Page<Account> findAll(Pageable pageable)
// Account saveAndFlush(Account entity)
// List<Account> saveAllAndFlush(Iterable<Account> entities)
// void flush()
// void deleteAllInBatch()
// void deleteAllByIdInBatch(Iterable<String> ids)
// Account getReferenceById(String id) -- lazy proxy, no DB hit
}
What is the repository hierarchy?
Spring Data's repository interfaces form a hierarchy. Each level adds more methods.
Repository<T, ID> (marker, no methods)
└─ CrudRepository<T, ID> (basic CRUD, Iterable returns)
└─ ListCrudRepository<T, ID> (same but List returns)
└─ PagingAndSortingRepository (findAll with Sort, Pageable)
└─ JpaRepository<T, ID> (flush, batch, lazy ref)
package com.bank.service;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class AccountListService {
private final AccountRepository accountRepo;
public AccountListService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional(readOnly = true)
public List<Account> findAll() {
return accountRepo.findAll();
// JpaRepository: returns List<Account> directly
// CrudRepository would return: Iterable<Account> (needs conversion!)
}
@Transactional(readOnly = true)
public Page<Account> findPaginated(int page, int size) {
return accountRepo.findAll(
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "balance")));
// Only available in JpaRepository (via PagingAndSortingRepository)
}
}
How is deleteAllInBatch different from deleteAll?
deleteAll() loads ALL entities into memory first, then deletes each one individually with separate DELETE statements. deleteAllInBatch() executes a single DELETE FROM table SQL – orders of magnitude faster but skips JPA lifecycle callbacks.
| Feature | deleteAll() | deleteAllInBatch() |
|---------|------------|-------------------|
| SQL | N SELECT + N DELETE | 1 DELETE FROM |
| Speed (10K rows) | ~30 minutes | ~2 seconds |
| Loads entities | Yes | No |
| @PreRemove callback | Fires | Skipped |
| Cascade | Honored | Skipped |
package com.bank.service;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CleanupService {
private final AccountRepository accountRepo;
public CleanupService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
// WRONG: deleteAll loads all entities then deletes one by one
@Transactional
public void wrongCleanup() {
accountRepo.deleteAll();
// SELECT * FROM accounts (loads 10K entities into memory)
// DELETE FROM accounts WHERE account_id = 'SAV0001'
// DELETE FROM accounts WHERE account_id = 'SAV0002'
// ... 10,000 individual DELETE statements
}
// CORRECT: deleteAllInBatch uses one SQL statement
@Transactional
public void correctCleanup() {
accountRepo.deleteAllInBatch();
// DELETE FROM accounts (one SQL statement, instant)
// Note: @PreRemove callbacks are NOT triggered
}
}
What is getReferenceById?
getReferenceById() returns a lazy proxy without hitting the database. It is efficient for setting foreign keys on child entities. This method is available only in JpaRepository.
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Trade;
import com.bank.repository.AccountRepository;
import com.bank.repository.TradeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TradePlacementService {
private final AccountRepository accountRepo;
private final TradeRepository tradeRepo;
public TradePlacementService(AccountRepository accountRepo,
TradeRepository tradeRepo) {
this.accountRepo = accountRepo;
this.tradeRepo = tradeRepo;
}
@Transactional
public void placeTrade(String accountId, String symbol, BigDecimal amount) {
// getReferenceById: NO database hit, just a proxy with the ID
Account accountRef = accountRepo.getReferenceById(accountId);
Trade trade = new Trade();
trade.setTradeId("TRD-099");
trade.setSymbol(symbol);
trade.setAmount(amount);
trade.setAccount(accountRef); // sets FK without loading Account
tradeRepo.save(trade);
// INSERT INTO trades (..., account_id) VALUES (..., 'SAV2001')
// Account was NEVER loaded from the database!
}
}
What is saveAndFlush?
saveAndFlush() persists the entity and immediately flushes the SQL to the database, instead of waiting until the transaction commits. Useful when you need the generated ID immediately or when you need the SQL to execute for database triggers.
package com.bank.service;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class AccountCreationService {
private final AccountRepository accountRepo;
public AccountCreationService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public Account createAccount(String id, String name, BigDecimal balance) {
Account account = new Account();
account.setAccountId(id);
account.setHolderName(name);
account.setBalance(balance);
// save(): queues INSERT, flushes at commit
// accountRepo.save(account);
// saveAndFlush(): INSERT executes immediately
Account saved = accountRepo.saveAndFlush(account);
// SQL fires NOW, not at commit time
// Useful when a DB trigger creates related records
return saved;
}
}
When should you use CrudRepository vs JpaRepository?
Use JpaRepository for most Spring Boot applications. Use CrudRepository only if you need technology-agnostic code that could switch between JPA and another Spring Data module.
package com.bank.repository;
import com.bank.model.Account;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.jpa.repository.JpaRepository;
// Technology-agnostic: works with JPA, MongoDB, Redis
// Missing: pagination, flush, batch delete, lazy references
public interface AccountCrudRepo extends CrudRepository<Account, String> {}
// JPA-specific: full-featured, recommended for Spring Boot
public interface AccountJpaRepo extends JpaRepository<Account, String> {}
What are common mistakes?
The most common mistake is using CrudRepository and then struggling with Iterable return types or missing pagination.
package com.bank.service;
import com.bank.model.Account;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
import java.util.stream.StreamSupport;
public class CrudRepoMistake {
// WRONG: CrudRepository returns Iterable, not List
public List<Account> wrongUsage(CrudRepository<Account, String> repo) {
Iterable<Account> iterable = repo.findAll();
// Must convert manually:
return StreamSupport.stream(iterable.spliterator(), false).toList();
}
// CORRECT: JpaRepository returns List directly
public List<Account> correctUsage(
org.springframework.data.jpa.repository.JpaRepository<Account, String> repo) {
return repo.findAll(); // List<Account> directly!
}
}
Visual Overview
Repository Hierarchy:
┌───────────────────────────┐
│ Repository<T, ID> │ (marker interface)
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ CrudRepository │ save, findById, findAll(Iterable)
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ ListCrudRepository │ findAll(List), findAllById(List)
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ PagingAndSortingRepo │ findAll(Sort), findAll(Pageable)
└─────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ JpaRepository │ flush, saveAndFlush,
│ │ deleteAllInBatch,
│ │ getReferenceById
└───────────────────────────┘
Key Takeaway
CrudRepository provides basic CRUD operations with Iterable return types and no pagination or batch features. JpaRepository extends it through several intermediate interfaces and adds everything a Spring Boot application needs: List return types, pagination with Pageable, sorting with Sort, immediate flush with saveAndFlush, efficient bulk deletion with deleteAllInBatch, and lazy proxies with getReferenceById.
At National Bank of India, Rajesh uses JpaRepository for the account service. findAll(PageRequest.of(0, 20, Sort.by(DESC, "balance"))) powers the account listing with server-side paging. deleteAllInBatch() removes 100,000 test records in 2 seconds. CrudRepository's deleteAll() would take 30 minutes because it loads all entities first then deletes one by one.
The hierarchy is: Repository → CrudRepository → ListCrudRepository → PagingAndSortingRepository → JpaRepository. Use JpaRepository for most Spring Boot applications. Use CrudRepository only if your code needs to be technology-agnostic, for example a module that could switch from JPA to MongoDB.
Remember one sentence: JpaRepository is a superset of CrudRepository that adds pagination, batch operations, flush control, and lazy references – always use it for Spring Boot apps.
---