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:

MethodBehaviour
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:

  1. At startup, Spring scans for interfaces extending Repository
  2. For each, it creates a SimpleJpaRepository<T, ID> implementation via JdkDynamicAopProxy
  3. The proxy intercepts method calls and routes them to SimpleJpaRepository or 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?

NeedExtend
Basic CRUD onlyCrudRepository
CRUD + pagination + sortingPagingAndSortingRepository
Full JPA features (flush, batch delete, reference)JpaRepository
+ Dynamic queries at runtimeJpaRepository + JpaSpecificationExecutor

In practice, most Spring Boot applications extend JpaRepository — the extra methods don’t hurt and are occasionally useful.


Key Takeaways

  • CrudRepositoryPagingAndSortingRepositoryJpaRepository — 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 = merge
  • deleteAllInBatch() is orders of magnitude faster than deleteAll() for bulk deletes
  • getReferenceById() returns a proxy without a SELECT — use it for setting FK references
  • saveAndFlush() 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.