RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#408 min readJul 11, 2026

CrudRepository vs JpaRepository.

practice
learning
Practice modeTest yourself instead of reading straight through

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.

🧠 Memory Map
Memory map — visual summary of this topic

What they are

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.

How Spring works under the hood

  1. You define an interface such as CrudRepository or JpaRepository for a JPA entity.
  2. At startup, Spring scans those interfaces and creates a runtime proxy, which is a generated object that stands in for your interface.
  3. The proxy usually delegates to Spring Data JPA’s default implementation, commonly SimpleJpaRepository, instead of you writing SQL manually.
  4. For basic CRUD calls, Spring translates the method into JPA EntityManager operations such as persist, merge, remove, and find.
  5. For derived query methods like findByEmail, Spring parses the method name and builds the query automatically.
  6. For 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.

Comparison at a glance

FeatureCrudRepositoryJpaRepository
Basic CRUDYesYes
Paging and sortingNoYes
Flush helpersNoYes
Batch delete helpersNoYes
Typical useSmall APIMost JPA apps

When and why to use each one

  1. Use CrudRepository if you truly only need the basic repository methods and want the smallest possible contract.
  2. Use 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.
  3. If you are building a web app with list pages, search results, or dashboards, JpaRepository is usually the practical default.
  4. If you are building a very small service or a teaching example, CrudRepository is fine and keeps the API simple.

Performance and edge cases

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.

  1. Every page load now pulls far too much data from the database.
  2. Memory usage spikes because the app materializes every order entity before sorting.
  3. The API response time jumps from under 100 ms to multiple seconds.
  4. GC pauses increase, logs show long SQL execution times, and users think the admin portal is frozen.

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.

Spring Boot
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:

  • Why does Spring Data offer both interfaces? 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.
  • What is the difference between 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.
  • When would you choose 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.
  • Why is paging so important? Paging keeps memory usage and response time under control by loading only a slice of data, such as 20 or 50 rows, instead of the whole table. It is essential for admin screens, search results, and APIs that can grow over time.
  • What does a batch delete change? Batch delete sends set-based SQL, which is faster than deleting one entity at a time. The trade-off is that entity callbacks and the first-level cache may not behave the same way as a normal managed delete.
  • Does 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.
  • Is 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.
  • Can a bulk delete leave stale data in memory? Yes. If entities are already loaded in the persistence context, a bulk operation can make the database and in-memory state disagree until you clear or refresh the context.

Tricky / gotcha questions:

  • Does 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.
  • Should you always extend 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.
  • Does 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.

Common Mistakes

  • Using findAll() on huge tables: This loads everything and can crush memory. Fix: use paging with JpaRepository.
  • Thinking 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.
  • Assuming save() only inserts: JPA may persist or merge depending on entity state. Fix: understand new, managed, detached, and removed entity states.
  • Ignoring bulk-operation side effects: Batch deletes can bypass callbacks and stale the persistence context. Fix: clear or refresh when you use bulk operations.

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.
  • Use paging for lists that can grow.
  • Use batch operations for maintenance jobs.
  • Do not confuse flush with commit.
  • Most JPA apps should start with JpaRepository.

Practice Tasks:

  • Build a simple entity and expose it first with CrudRepository, then switch to JpaRepository and add paging.
  • Add a findByEmail method and test how Spring derives the query from the name.
  • Create a batch-delete endpoint and observe how the behavior differs from deleting one entity at a time.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

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> { }