Hook: Interviewers like this question because it looks tiny, but it checks whether you understand Spring Data’s layering and when the extra JPA features actually matter.
Question: CrudRepository vs JpaRepository.
Answer: CrudRepository gives you the basic create, read, update, and delete methods for a repository. JpaRepository includes all of that and adds JPA-friendly features like paging, sorting, flushing, and batch deletes. In simple terms: use CrudRepository for the smallest possible API, and use JpaRepository when you are working with JPA entities and want the richer feature set.
Interview-Ready Answer: In my projects, I usually pick JpaRepository for JPA entities because it gives me the full CRUD API plus useful extras like paging, sorting, flush(), and batch operations. CrudRepository is the leaner option if I only need the basic methods and want to keep the contract minimal. So the main difference is that JpaRepository is a superset, not a different way of doing repositories.
CrudRepository is the smallest standard Spring Data repository. It focuses on the core operations every beginner expects: save, find, delete, and count. JpaRepository is the richer Spring Data JPA interface for entity-based work; it includes the CRUD contract and adds features that make sense when the backing store is a JPA persistence context, meaning the managed set of entities tracked by the EntityManager.
CrudRepository or JpaRepository for a JPA entity.SimpleJpaRepository, instead of you writing SQL manually.EntityManager operations such as persist, merge, remove, and find.findByEmail, Spring parses the method name and builds the query automatically.JpaRepository-only helpers such as paging, sorting, flush(), or batch deletes, Spring uses JPA-aware behavior that fits the persistence context and transaction boundary.That means the real difference is not just “more methods”. It is also that JpaRepository gives you JPA-specific operations that work nicely with the lifecycle of entities.
| Feature | CrudRepository | JpaRepository |
|---|---|---|
| Basic CRUD | Yes | Yes |
| Paging and sorting | No | Yes |
| Flush helpers | No | Yes |
| Batch delete helpers | No | Yes |
| Typical use | Small API | Most JPA apps |
CrudRepository if you truly only need the basic repository methods and want the smallest possible contract.JpaRepository for almost all real JPA applications, because you usually need paging for screens, sorting for reports, and flush or batch operations for admin or maintenance jobs.JpaRepository is usually the practical default.CrudRepository is fine and keeps the API simple.The Java-side cost of either interface is tiny; both are just method calls into a proxy. The real cost comes from the database. A primary-key lookup with findById is usually fast because the ID column is indexed; in practice it is often a low single-digit millisecond operation inside a healthy service, but network and database load matter more than the interface choice. findAll(), on the other hand, is dangerous on large tables because it can load thousands or millions of rows into memory. That can turn a small request into a slow query, a large heap allocation, and a lot of garbage collection.
Another important detail: save() does not always mean “insert”. For a new entity, JPA typically persists it; for a detached entity, it may merge it. That is why understanding entity state matters. Also, batch methods like deleteAllInBatch() are fast because they execute bulk SQL, but they can bypass entity callbacks and leave already-loaded entities stale until the persistence context is cleared or refreshed. So JpaRepository is not just “more methods”; it also gives you tools that can be powerful or dangerous depending on how you use them.
Memory hook: think of CrudRepository as the basic toolbox and JpaRepository as the same toolbox plus a paging ruler, a flush button, and a bulk-delete lever.
Real-World Story: Imagine a checkout platform with an admin screen that shows recent orders. A developer starts with CrudRepository, then writes code that calls findAll() and sorts the orders in Java because it is the easiest thing to do. It works in testing with 200 rows, but in production the table grows to 400,000 rows.
The fix is to switch to JpaRepository and use findAll(Pageable) with a page size like 20 or 50, plus database sorting. If the team also needs housekeeping jobs, deleteAllInBatch() can clean up old records efficiently without loading them one by one. This is a classic case where the interface choice directly affects scalability and user experience.
What goes wrong when misunderstood: the bug is usually not a crash right away; it is slow queries, high heap usage, and timeouts. The symptoms are long request logs, repeated full-table scans, and users reporting that lists never finish loading.
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@SpringBootApplication
public class CrudVsJpaApplication {
public static void main(String[] args) {
// Default properties keep the example fully runnable without an external application.properties file.
SpringApplication app = new SpringApplication(CrudVsJpaApplication.class);
app.setDefaultProperties(Map.of(
"spring.datasource.url", "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL",
"spring.datasource.driverClassName", "org.h2.Driver",
"spring.datasource.username", "sa",
"spring.datasource.password", "",
"spring.jpa.hibernate.ddl-auto", "create-drop",
"spring.jpa.show-sql", "true"
));
app.run(args);
}
@Bean
CommandLineRunner demo(CrudCustomerRepository crudRepository, CustomerJpaRepository jpaRepository) {
return args -> {
// CrudRepository: the basic create/read/update/delete path.
Customer savedByCrud = crudRepository.save(new Customer(null, "Ava", "ava@example.com"));
crudRepository.saveAll(List.of(
new Customer(null, "Ben", "ben@example.com"),
new Customer(null, "Cara", "cara@example.com")
));
System.out.println("CrudRepository count = " + crudRepository.count());
// JpaRepository: same CRUD methods, plus paging and sorting.
Page<Customer> firstPage = jpaRepository.findAll(PageRequest.of(0, 2, Sort.by("name")));
System.out.println("JpaRepository page size = " + firstPage.getContent().size());
System.out.println("JpaRepository total elements = " + firstPage.getTotalElements());
// JPA-specific helper: saveAndFlush forces SQL execution now, which can matter before a follow-up query.
Customer flushed = jpaRepository.saveAndFlush(new Customer(null, "Dina", "dina@example.com"));
System.out.println("Saved and flushed id = " + flushed.getId());
// Batch delete is efficient, but it bypasses entity-by-entity removal.
jpaRepository.deleteAllInBatch(List.of(flushed));
System.out.println("Exists after batch delete = " + jpaRepository.existsById(flushed.getId()));
// Edge case: a missing row returns Optional.empty, so you should handle the not-found path explicitly.
try {
Customer missing = crudRepository.findById(999L)
.orElseThrow(() -> new NoSuchElementException("Customer 999 not found"));
System.out.println(missing);
} catch (NoSuchElementException ex) {
System.out.println("Handled edge case: " + ex.getMessage());
}
System.out.println("Saved by CrudRepository = " + savedByCrud.getId());
};
}
}
@Entity
@Table(name = "customers")
class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(nullable = false, unique = true)
private String email;
protected Customer() {
// JPA needs a no-args constructor so it can build entities through reflection.
}
Customer(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Customer{id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + '}';
}
}
interface CrudCustomerRepository extends CrudRepository<Customer, Long> {
}
interface CustomerJpaRepository extends JpaRepository<Customer, Long> {
}Follow-up & Tricky Questions:
CrudRepository is the minimal contract, while JpaRepository is the practical JPA-friendly one. Spring gives you a smaller or richer option depending on how much behavior you need.save() and saveAndFlush()? save() schedules the change in the persistence context, while saveAndFlush() forces the SQL to hit the database immediately. Flush is not the same as commit; it just pushes pending changes out sooner.CrudRepository in a real project? Usually only when the domain is very small or you want to keep the repository contract intentionally tiny. For most JPA applications, JpaRepository is the better default.JpaRepository make SQL faster than CrudRepository? No. The interface does not change database speed by itself; it simply gives you more useful operations. Query shape, indexes, and pagination are what usually determine performance.save() always an insert? No. In JPA, the same call may insert a new entity or merge an existing detached one. That is a common trap for candidates who think repository methods map one-to-one with SQL statements.Tricky / gotcha questions:
CrudRepository mean you cannot page at all? Correct — the paging API comes from richer repository types such as JpaRepository. If you need pages, sorting, or slices, CrudRepository alone is not enough.JpaRepository because it has more methods? Usually yes for JPA apps, but not because more is always better. If you want a very narrow API to limit what the rest of the code can do, CrudRepository can be a deliberate design choice.flush() commit the transaction? No, it only synchronizes pending changes with the database; the transaction can still roll back later. That difference matters in tests and in multi-step business logic.findAll() on huge tables: This loads everything and can crush memory. Fix: use paging with JpaRepository.JpaRepository is a different persistence technology: It is not; it is still Spring Data on top of JPA. Fix: treat it as a richer interface, not a new framework.save() only inserts: JPA may persist or merge depending on entity state. Fix: understand new, managed, detached, and removed entity states.Memory Hook: CrudRepository is the basic toolbox; JpaRepository is the same toolbox with paging, flush, and batch tools added.
Cheat Sheet:
CrudRepository = basic CRUD only.JpaRepository = CRUD + paging/sorting + JPA helpers.flush with commit.JpaRepository.Practice Tasks:
CrudRepository, then switch to JpaRepository and add paging.findByEmail method and test how Spring derives the query from the name.