Repository Interfaces: CrudRepository, JpaRepository, and How They Work
Introduction
Spring Data JPA’s repository abstraction eliminates the DAO boilerplate that every Java application used to require. You declare an interface, extend one of the repository base interfaces, and Spring generates a complete implementation at startup — find, save, delete, count, and more — with zero code written.
The Repository Hierarchy
Repository<T, ID> ← Marker interface — no methods
│
└── CrudRepository<T, ID> ← Basic CRUD: save, find, delete, count
│
└── PagingAndSortingRepository<T, ID> ← + findAll(Pageable), findAll(Sort)
│
└── JpaRepository<T, ID> ← + flush, saveAndFlush, deleteInBatch
Each interface adds more operations. Pick the one that gives you what you need — no more.
CrudRepository
Provides basic CRUD operations:
public interface CrudRepository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
Use when you only need basic CRUD and do not need pagination or JPA-specific operations.
PagingAndSortingRepository
Adds pagination and sorting:
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
Use when you need paginated or sorted results but do not need JPA-specific flush behaviour.
JpaRepository
The most commonly used interface. Extends PagingAndSortingRepository and adds JPA-specific operations:
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
void deleteAllInBatch(Iterable<T> entities);
void deleteAllByIdInBatch(Iterable<ID> ids);
void deleteAllInBatch();
T getReferenceById(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
Notable additions over PagingAndSortingRepository:
| Method | Behaviour |
|---|---|
flush() | Immediately flushes pending SQL to the database |
saveAndFlush(entity) | Saves and immediately flushes (returns updated entity) |
deleteAllInBatch() | Single DELETE FROM table — much faster than deleteAll() for large tables |
deleteAllByIdInBatch(ids) | DELETE FROM table WHERE id IN (...) — single query |
getReferenceById(id) | Returns a proxy (no SQL) — for setting FK references |
Creating a Repository
Extend JpaRepository with your entity type and primary key type:
package com.devopsmonk.jpademo.repository;
import com.devopsmonk.jpademo.domain.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Empty interface — Spring generates all JpaRepository methods automatically
}
Spring Boot auto-detects interfaces extending Spring Data repository types and creates implementations at startup. No @Repository annotation needed (though it does not hurt).
Using the Repository
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
// Create
@Transactional
public Product create(String name, BigDecimal price) {
Product product = new Product();
product.setName(name);
product.setPrice(price);
return productRepository.save(product);
}
// Read
public Product findById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Product not found: " + id));
}
// Read all
public List<Product> findAll() {
return productRepository.findAll();
}
// Update
@Transactional
public Product updatePrice(Long id, BigDecimal newPrice) {
Product product = productRepository.findById(id).orElseThrow();
product.setPrice(newPrice);
// dirty checking — no explicit save() needed within @Transactional
return product;
}
// Delete
@Transactional
public void delete(Long id) {
productRepository.deleteById(id);
}
// Check existence
public boolean exists(Long id) {
return productRepository.existsById(id);
}
// Count
public long count() {
return productRepository.count();
}
}
save() vs saveAndFlush()
save() does not immediately execute SQL — it registers the entity in the persistence context and flushes at transaction commit.
saveAndFlush() executes the SQL immediately:
// save() — SQL runs at transaction commit
Product saved = productRepository.save(product);
// saved.getId() is populated (Hibernate executed INSERT for IDENTITY strategy)
// but surrounding queries in the same tx might not see this yet
// saveAndFlush() — SQL runs immediately, result visible to subsequent queries
Product saved = productRepository.saveAndFlush(product);
// guaranteed visible to all subsequent queries in this transaction
Use saveAndFlush() when you need to guarantee the persisted data is readable by a subsequent query within the same transaction. In most cases, save() is sufficient.
deleteAll() vs deleteAllInBatch()
// deleteAll() — loads all entities into memory, then deletes one by one
productRepository.deleteAll();
// SQL: SELECT * FROM products
// DELETE FROM products WHERE id = 1
// DELETE FROM products WHERE id = 2
// ... (N SQL statements)
// deleteAllInBatch() — single SQL, no entities loaded into memory
productRepository.deleteAllInBatch();
// SQL: DELETE FROM products
Always prefer deleteAllInBatch() for bulk deletes on large tables.
getReferenceById() — Proxy for FK References
getReferenceById(id) returns a proxy (no SELECT executed) — useful when you need to set a foreign key reference without loading the full entity:
@Transactional
public OrderItem createItem(Long orderId, Long productId, int quantity) {
// getReferenceById — no SELECT for order or product
Order order = orderRepository.getReferenceById(orderId);
Product product = productRepository.getReferenceById(productId);
OrderItem item = new OrderItem();
item.setOrder(order); // sets the FK reference via proxy
item.setProduct(product);
item.setQuantity(quantity);
item.setUnitPrice(product.getPrice()); // this triggers product proxy load
return itemRepository.save(item);
}
getReferenceById() replaces the old getOne() method (deprecated in Spring Data JPA 3.x).
Multiple Repository Interfaces
You can extend multiple repository interfaces:
public interface ProductRepository
extends JpaRepository<Product, Long>,
JpaSpecificationExecutor<Product> {
// JpaSpecificationExecutor adds: findAll(Specification), findOne(Specification), count(Specification)
}
JpaSpecificationExecutor is covered in Article 21 (Specifications).
How Spring Generates Repository Implementations
Spring Data JPA uses AOP and dynamic proxies to generate repository implementations:
- At startup, Spring scans for interfaces extending
Repository - For each, it creates a
SimpleJpaRepository<T, ID>implementation viaJdkDynamicAopProxy - The proxy intercepts method calls and routes them to
SimpleJpaRepositoryor query execution engines
You can see the generated class name in debug logs:
Creating shared instance of singleton bean 'productRepository'
The actual implementation is com.sun.proxy.$Proxy67 delegating to SimpleJpaRepository.
Which Repository Interface to Extend?
| Need | Extend |
|---|---|
| Basic CRUD only | CrudRepository |
| CRUD + pagination + sorting | PagingAndSortingRepository |
| Full JPA features (flush, batch delete, reference) | JpaRepository |
| + Dynamic queries at runtime | JpaRepository + JpaSpecificationExecutor |
In practice, most Spring Boot applications extend JpaRepository — the extra methods don’t hurt and are occasionally useful.
Key Takeaways
CrudRepository→PagingAndSortingRepository→JpaRepository— each adds more operations- Spring generates implementations at startup — no boilerplate DAO code needed
save()on a new entity = persist;save()on a detached entity = mergedeleteAllInBatch()is orders of magnitude faster thandeleteAll()for bulk deletesgetReferenceById()returns a proxy without a SELECT — use it for setting FK referencessaveAndFlush()guarantees the SQL runs immediately within the transaction
What’s Next
Article 17 covers derived query methods — how Spring Data JPA converts method names like findByEmailAndStatus into SQL queries automatically, with all the keywords and operators available.