Spring Data JPA Basics
Reviewed & published by Brayan K
Declare an interface extending JpaRepository (or CrudRepository) and Spring Data generates the implementation — giving you save, findById, findAll and delete with zero boilerplate.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
Make sure you understand the repository pattern and @Repository, and ideally have seen JDBC to appreciate how much boilerplate Spring Data removes.
What You'll Learn
- How Spring Data generates repositories
- JpaRepository vs CrudRepository
- The <Entity, Id> type parameters
- Free methods: save / findById / findAll / delete
- Derived query methods from method names
- Injecting the generated repository into a service
The Big Idea — Ordering From a Menu, Not Cooking
💡 Analogy: Writing data access by hand is like cooking every dish yourself. Spring Data JPA is like ordering from a menu: you simply name what you want (declare an interface, write a method like findByLastName) and the kitchen (Spring Data) prepares it. save, findById, findAll and delete are the standing menu — always available, no recipe required.
You describe what you want; Spring Data implements the how.
1️⃣ Declare a Repository Interface
Extend JpaRepository<Entity, Id> and Spring Data generates the implementation at runtime. Add derived query methods just by naming them.
// (Conceptual — needs Spring Data JPA on the classpath to run.)
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// Declare ONLY the interface. Spring Data GENERATES the implementation at
// runtime and registers it as a bean — no boilerplate code to write.
// <User, Long> = <entity type, id type>
public interface UserRepository extends JpaRepository<User, Long> {
// A DERIVED QUERY METHOD: Spring parses the name into a query.
List<User> findByLastName(String lastName);
}2️⃣ The Methods You Get for Free
Extending the base type gives you full CRUD — save, findById, findAll, count, deleteById — with no code to write.
// METHODS YOU GET FOR FREE from CrudRepository / JpaRepository:
//
// save(entity) -> insert or update, returns the saved entity
// findById(id) -> Optional<T> (empty if not found)
// existsById(id) -> boolean
// findAll() -> all entities (a List for JpaRepository)
// count() -> long, number of entities
// deleteById(id) -> remove by primary key
// delete(entity) -> remove a given entity
//
// JpaRepository adds JPA extras such as flush(), saveAndFlush(), and
// deleteAllInBatch(). You never implement any of these yourself.3️⃣ Using the Generated Repository
Inject the repository interface into a service and call its methods. The same clean layering as before — service depends on repository — with the implementation provided automatically.
package com.example.demo;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository users; // the GENERATED repository is injected
public UserService(UserRepository users) {
this.users = users;
}
public User create(User u) { return users.save(u); } // CRUD
public Optional<User> find(long id) { return users.findById(id); }
public List<User> all() { return users.findAll(); }
public void remove(long id) { users.deleteById(id); }
public List<User> byLastName(String n){ return users.findByLastName(n); } // derived
}4️⃣ What CRUD Looks Like, Concretely
To make the generated methods tangible, here is a plain-Java stand-in with real output, showing exactly the save / findById / count / delete behaviour Spring Data gives you.
import java.util.*;
public class Main {
record User(long id, String firstName, String lastName) {}
// Simulate what a Spring Data CrudRepository GIVES YOU for free, in plain
// Java, so save/findById/findAll/deleteById are concrete and runnable.
static class FakeCrudRepository {
private final Map<Long, User> store = new LinkedHashMap<>();
User save(User u) { store.put(u.id(), u); return u; }
Optional<User> findById(long id) { return Optional.ofNullable(store.get(id)); }
List<User> findAll() { return new ArrayList<>(store.values()); }
long count() { return store.size(); }
void deleteById(long id) { store.remove(id); }
}
public static void main(String[] args) {
FakeCrudRepository repo = new FakeCrudRepository();
repo.save(new User(1, "Ada", "Lovelace"));
repo.save(new User(2, "Linus", "Torvalds"));
System.out.println("count: " + repo.count());
System.out.println("findById 1: " + repo.findById(1).map(User::firstName).orElse("none"));
repo.deleteById(1);
System.out.println("after delete count: " + repo.count());
System.out.println("findAll: " + repo.findAll().size() + " user(s)");
}
}import java.util.*;
import java.util.stream.*;
public class Main {
// 🎯 YOUR TURN — replace each ___ using the hint beside it.
//
// Spring Data reads your METHOD NAME and writes the query from it. This
// is that parser, small enough to read in one sitting.
record Book(String title, String author, int pages) {}
static List<Book> shelf = List.of(
new Book("Dune", "Herbert", 412),
new Book("Emma", "Austen", 474),
new Book("Bee", "Austen", 96)
);
static List<Book> derive(String methodName, Object... args) {
// 1) Everything after the prefix is the query.
String tail = methodName.replaceFirst("^___", ""); // 👉 replace ___ with findBy
// 2) Each condition is separated by this keyword.
String[] parts = tail.split("___"); // 👉 replace ___ with And
Stream<Book> s = shelf.stream();
int i = 0;
for (String part : parts) {
Object arg = args[i++];
// 3) Keep only the rows that pass — one filter per condition.
if (part.equals("Author")) s = s.___(b -> b.author().equals(arg)); // 👉 replace ___ with filter
else if (part.equals("PagesGreaterThan")) s = s.filter(b -> b.pages() > (int) arg);
else if (part.equals("TitleContaining")) s = s.filter(b -> b.title().contains((String) arg));
// 4) A word it does not know is a STARTUP failure in Spring, not a
// runtime one — the method name is checked when the app boots.
else ___ new IllegalArgumentException("Cannot parse: " + part); // 👉 replace ___ with throw
}
// 5) Turn the stream back into a list.
return s.___(Collectors.toList()); // 👉 replace ___ with collect
}
public static void main(String[] args) {
System.out.println("findByAuthor(Austen): " + derive("findByAuthor", "Austen").stream().map(Book::title).toList());
System.out.println("findByPagesGreaterThan(100): " + derive("findByPagesGreaterThan", 100).stream().map(Book::title).toList());
System.out.println("findByAuthorAndPagesGreaterThan(Austen, 100): " + derive("findByAuthorAndPagesGreaterThan", "Austen", 100).stream().map(Book::title).toList());
try {
derive("findByColour", "blue");
} catch (IllegalArgumentException e) {
System.out.println("Unparseable method name: " + e.getMessage());
}
}
}🧠 Quick Recall
Predict before you peek.
Answer: Spring Data generates it at runtime — you only declare the interface.
Answer: an Optional<T>, empty when no entity has that id.
Answer: the entity type and the id type — e.g. JpaRepository<User, Long>.
Common Errors
- ❌ Writing an implementation class. Unnecessary. Fix: declare only the interface; Spring Data does the rest.
- ❌ Wrong id type parameter. Method signatures break. Fix: match the entity's primary-key type.
- ❌ Treating findById as returning the entity. It's an Optional. Fix: unwrap with orElse/map.
- ❌ Mistyped derived method names. e.g. findByLstName. Fix: match the entity property names exactly.
- ❌ Overusing derived methods for complex queries. Fix: use @Query (JPQL/native) when names get unwieldy.
Pro Tips
- 💡 Extend JpaRepository for the full feature set on a JPA-backed app.
- 💡 Embrace Optional from findById — handle "not found" explicitly, no nulls.
- 💡 Name derived methods carefully — they must match entity property names.
- 💡 Drop to @Query for queries too complex to express as a method name.
📋 Quick Reference
| Method / type | Signature / idea | Note |
|---|---|---|
| base type | JpaRepository<T, ID> | entity, id |
| save | save(entity) | insert/update |
| findById | Optional<T> findById(id) | empty if absent |
| findAll | List<T> findAll() | all rows |
| count | long count() | how many |
| deleteById | deleteById(id) | remove by key |
| derived | findByLastName(name) | from method name |
📚 Resources / Keep Learning
- 📘 Spring Data JPA — Getting Started (docs)
- 📗 Derived Query Methods — Spring Data docs
- 📗 Accessing Data with JPA — spring.io guide
🎉 Lesson Complete!
You now know how Spring Data JPA generates a repository implementation from an interface, the difference between JpaRepository and CrudRepository, the <Entity, Id> type parameters, the free CRUD methods (save/findById/findAll/delete), and derived query methods.
Practice quiz
What does Spring Data JPA do for a repository interface?
- Nothing automatic
- Generates the implementation at runtime from the interface
- Compiles it to native code
- Renders it as HTML
Answer: Generates the implementation at runtime from the interface. You declare an interface extending a Spring Data base type, and Spring generates a working implementation at runtime.
Which interface adds JPA-specific features on top of CrudRepository?
- MongoRepository
- JpaRepository
- WebRepository
- HttpRepository
Answer: JpaRepository. JpaRepository extends PagingAndSortingRepository/CrudRepository and adds JPA-specific methods like flush and batch operations.
What are the two type parameters of JpaRepository<T, ID>?
- The entity type and its id type
- The table name and column name
- The request and response
- Two unrelated services
Answer: The entity type and its id type. JpaRepository<T, ID> is parameterised by the entity type T and the type of its primary key ID.
Which method does CrudRepository provide to persist an entity?
- render()
- save()
- route()
- scan()
Answer: save(). save() inserts or updates the given entity and returns the saved instance.
What does findById return in Spring Data?
- the entity or null
- an Optional<T>
- a boolean
- a List<T>
Answer: an Optional<T>. findById returns Optional<T>, which is empty when no entity has that id.
What does findAll() return?
- a single entity
- an Iterable/List of all entities
- a count
- void
Answer: an Iterable/List of all entities. findAll() returns all entities of the type as an Iterable (a List for JpaRepository).
How do you delete an entity by its id?
- drop(id)
- remove(id)
- deleteById(id)
- purge(id)
Answer: deleteById(id). deleteById(ID) removes the entity with the given primary key.
A method named findByLastName in a repository interface is a...
- compile error
- derived query method generated from its name
- static method
- controller mapping
Answer: derived query method generated from its name. Spring Data parses method names like findByLastName and derives the query automatically.
Do you write the implementation class for a JpaRepository interface?
- Yes, always by hand
- No — Spring Data provides it at runtime
- Only in tests
- Only for delete methods
Answer: No — Spring Data provides it at runtime. You only declare the interface; Spring Data generates and registers the implementation as a bean for you.
CrudRepository's count() returns...
- the number of entities
- the first entity
- a boolean
- the id
Answer: the number of entities. count() returns the total number of entities of that type as a long.
Continue this course
- Previous: Repositories & @Repository
- Next: JPA Entities & Mapping — @Entity, @Id, @GeneratedValue, @Column and @Table
- Quick reference: Java cheat sheet
Frequently asked questions
What is Spring Data JPA and what does it give me?
Spring Data JPA is a module that dramatically reduces the boilerplate of data access on top of JPA/Hibernate. Instead of writing a repository implementation by hand, you declare an interface that extends one of Spring Data's base types — typically JpaRepository<T, ID> — and Spring Data generates a fully working implementation at runtime and registers it as a bean you can inject. Out of the box you get standard CRUD operations (save, findById, findAll, count, deleteById and more) without writing any code. You can also add derived query methods simply by naming them, and write custom queries with @Query when needed. It is the repository pattern from the previous lesson, automated.
What is the difference between CrudRepository and JpaRepository?
They are layers of the same hierarchy. CrudRepository<T, ID> is the most basic interface, providing generic CRUD methods: save, findById (returning Optional), existsById, findAll, count, deleteById and delete. PagingAndSortingRepository extends it with pagination and sorting. JpaRepository extends those and adds JPA-specific conveniences such as flush(), saveAndFlush(), batch delete operations (deleteAllInBatch), and returns List rather than Iterable from findAll for convenience. For a JPA-backed application you typically extend JpaRepository to get the full set; choose CrudRepository if you want to keep the surface minimal or stay storage-agnostic. Either way, Spring Data supplies the implementation.
What are the two type parameters in JpaRepository<User, Long>?
The first is the entity type the repository manages, and the second is the type of that entity's primary key (its id). So JpaRepository<User, Long> means 'a repository of User entities whose id is a Long'. These type parameters let Spring Data generate strongly-typed methods: save accepts and returns a User, findById accepts a Long and returns Optional<User>, findAll returns List<User>, and deleteById accepts a Long. Getting these parameters right is essential because they drive the signatures of every inherited method and the derived query methods you declare. The entity itself is mapped to a table via JPA annotations, which is covered in the next lesson.
How do derived query methods work?
Spring Data parses the names of methods you declare in your repository interface and derives a query from them automatically — you never write the body. A method named findByLastName(String lastName) becomes a query selecting users whose lastName matches the argument; findByFirstNameAndLastName, findByAgeGreaterThan, countByActiveTrue and existsByEmail all follow the same convention of a verb (find/count/exists/delete) plus property expressions joined by And/Or and optional keywords. This gives you concise, type-safe queries for common cases with zero implementation code. For anything too complex to express in a method name, you fall back to the @Query annotation with JPQL or native SQL.
Do I have to implement the repository interface myself?
No — that is the whole point. You declare the interface extending JpaRepository (or CrudRepository) and Spring Data creates a proxy implementation at startup, registers it as a bean, and injects it wherever you ask for the interface. You write no implementation class and no SQL for the standard methods. Your job is to declare the interface, define any derived query methods by naming, and add @Query for the rare complex cases. This is why understanding the repository pattern first matters: Spring Data is delivering exactly that abstraction, with the implementation generated for you, so your service layer simply injects the interface and calls save, findById, findAll and deleteById.