Why Spring Data JPA Repositories Must Be Interfaces, Not Classes
Trying to create a Spring Data repository by extending JpaRepository with a class instead of an interface? It won't work. Here's why Spring Data JPA requires interfaces and what happens behind the scenes.
The Questionβ
Why does this work:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
But this doesn't:
public class UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Short answer: Spring Data JPA uses proxy pattern to generate implementations at runtime. Proxies can only be created from interfaces, not classes.
The Correct Approach: Interfaceβ
What You Writeβ
package com.ecommerce.app.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// Just declare what you want - no implementation!
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
boolean existsByEmail(String email);
}
Note: This is an interface (not a class)
What Spring Generates (Behind the Scenes)β
Spring creates a proxy implementation at runtime:
// Spring automatically generates this (simplified):
public class UserRepositoryImpl implements UserRepository {
private EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
TypedQuery<User> query = entityManager.createQuery(
"SELECT u FROM User u WHERE u.email = :email",
User.class
);
query.setParameter("email", email);
return query.getResultStream().findFirst();
}
@Override
public Optional<User> findByUsername(String username) {
// Spring generates this too...
}
@Override
public boolean existsByEmail(String email) {
// And this...
}
// Plus all 20+ methods from JpaRepository:
// save(), findById(), findAll(), delete(), etc.
}
You never see this code - Spring generates it automatically!
How to Use Itβ
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository; // β
Spring injects the proxy
@GetMapping("/{email}")
public User getUserByEmail(@PathVariable String email) {
return userRepository.findByEmail(email) // β
Works perfectly
.orElseThrow(() -> new NotFoundException("User not found"));
}
}
It works! Spring's generated proxy handles everything.
The Wrong Approach: Classβ
What You Might Tryβ
// β WRONG - This won't work!
public class UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
}
Error you'll get:
Compile error: JpaRepository is an interface, cannot be extended by a class
Even If You Could... (Let's Pretend)β
Even if Java allowed this, you'd face problems:
Problem 1: JpaRepository is Interface with 20+ Abstract Methodsβ
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID> {
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);
// ... and 10+ more methods!
}
If you use a class, you'd have to implement ALL of these manually!
// You'd have to write all this yourself:
public class UserRepository implements JpaRepository<User, Long> {
@Override
public void flush() {
// You implement this... how?
}
@Override
public <S extends User> S saveAndFlush(S entity) {
// And this...
}
@Override
public void deleteAllInBatch(Iterable<User> entities) {
// And this...
}
// ... 20+ more methods to implement!
// Plus YOUR custom methods:
@Override
public Optional<User> findByEmail(String email) {
// And somehow implement this too!
}
}
Nobody wants to write all that boilerplate!
Problem 2: Spring's Proxy Won't Workβ
Spring Data JPA uses JDK Dynamic Proxies which only work with interfaces:
// How Spring creates repository proxies (simplified):
JpaRepositoryFactory factory = new JpaRepositoryFactory(entityManager);
// This only works with INTERFACES:
UserRepository proxy = factory.getRepository(UserRepository.class); // β
// Can't create proxy from a CLASS:
UserRepositoryClass proxy = factory.getRepository(UserRepositoryClass.class); // β
Why Interfaces? The Real Reasonβ
The Menu vs Chef Analogyβ
Think of it like a restaurant:
The Interface = The Menuβ
public interface Menu {
Dish serveBreakfast();
Dish serveLunch();
Dish serveDinner();
}
What's on the menu:
- Breakfast (what you want)
- Lunch (what you want)
- Dinner (what you want)
Menu doesn't tell you HOW to cook - it just lists what's available.
The Implementation = The Chefβ
The chef knows HOW to make these dishes:
public class Chef implements Menu {
@Override
public Dish serveBreakfast() {
// Chef knows how to make breakfast
return new Dish("Eggs and bacon");
}
@Override
public Dish serveLunch() {
// Chef knows how to make lunch
return new Dish("Sandwich");
}
@Override
public Dish serveDinner() {
// Chef knows how to make dinner
return new Dish("Steak and potatoes");
}
}
Spring Data = The Master Chefβ
Here's the magic: With Spring Data JPA, Spring IS the chef!
You provide the menu (interface):
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); // "I want this dish"
List<User> findByAgeGreaterThan(int age); // "And this dish"
}
Spring creates the chef (implementation) automatically:
// Spring analyzes your method names:
// "findByEmail" β SELECT * FROM users WHERE email = ?
// "findByAgeGreaterThan" β SELECT * FROM users WHERE age > ?
// And generates the implementation:
public class SpringGeneratedChef implements UserRepository {
@Override
public Optional<User> findByEmail(String email) {
return entityManager.createQuery(
"SELECT u FROM User u WHERE u.email = :email", User.class
)
.setParameter("email", email)
.getResultStream()
.findFirst();
}
@Override
public List<User> findByAgeGreaterThan(int age) {
return entityManager.createQuery(
"SELECT u FROM User u WHERE u.age > :age", User.class
)
.setParameter("age", age)
.getResultList();
}
// Plus all the JpaRepository methods...
}
You never write this code - Spring writes it for you!
How Spring Generates Implementationsβ
Step 1: You Define the Interfaceβ
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategory(String category);
List<Product> findByPriceLessThan(Double price);
}
Step 2: Spring Scans at Startupβ
Application startup...
Scanning for @Repository interfaces...
Found: ProductRepository extends JpaRepository
Analyzing methods...
- findByCategory(String) β Query: WHERE category = ?
- findByPriceLessThan(Double) β Query: WHERE price < ?
Step 3: Spring Creates Proxyβ
// Spring creates a dynamic proxy:
Class<?> proxyClass = Proxy.newProxyInstance(
classLoader,
new Class[] { ProductRepository.class }, // Interface to implement
new JpaRepositoryInvocationHandler() // Handler with query logic
);
ProductRepository proxy = (ProductRepository) proxyClass.newInstance();
Step 4: Proxy Handles Method Callsβ
// When you call:
productRepository.findByCategory("Electronics");
// The proxy intercepts and:
// 1. Parses method name: "findByCategory"
// 2. Generates query: "SELECT p FROM Product p WHERE p.category = ?"
// 3. Sets parameter: "Electronics"
// 4. Executes query
// 5. Returns List<Product>
All automatic!
Method Name Pattern Matchingβ
Spring Data JPA analyzes your method names to generate queries:
Supported Keywordsβ
| Method Name | Generated Query |
|---|---|
findByEmail(String email) | WHERE email = ? |
findByEmailAndPassword(String email, String password) | WHERE email = ? AND password = ? |
findByAgeGreaterThan(int age) | WHERE age > ? |
findByAgeLessThanEqual(int age) | WHERE age <= ? |
findByNameContaining(String name) | WHERE name LIKE %?% |
findByNameStartingWith(String prefix) | WHERE name LIKE ?% |
findByNameEndingWith(String suffix) | WHERE name LIKE %? |
findByEmailIsNull() | WHERE email IS NULL |
findByEmailIsNotNull() | WHERE email IS NOT NULL |
findByActiveTrue() | WHERE active = true |
findByActiveFalse() | WHERE active = false |
findByAgeIn(Collection<Integer> ages) | WHERE age IN (?) |
findByAgeBetween(int start, int end) | WHERE age BETWEEN ? AND ? |
findByOrderByNameAsc() | ORDER BY name ASC |
findByOrderByNameDesc() | ORDER BY name DESC |
Examplesβ
public interface UserRepository extends JpaRepository<User, Long> {
// Simple equals
Optional<User> findByEmail(String email);
// Multiple conditions
List<User> findByUsernameAndActiveTrue(String username);
// Comparisons
List<User> findByAgeGreaterThan(int age);
List<User> findByAgeBetween(int minAge, int maxAge);
// String operations
List<User> findByEmailContaining(String emailPart);
List<User> findByUsernameStartingWith(String prefix);
// Null checks
List<User> findByEmailIsNull();
List<User> findByEmailIsNotNull();
// In clause
List<User> findByAgeIn(List<Integer> ages);
// Ordering
List<User> findByActiveOrderByCreatedAtDesc(boolean active);
// Existence checks
boolean existsByEmail(String email);
// Count
long countByActive(boolean active);
// Delete
void deleteByEmail(String email);
}
Spring generates SQL for ALL of these automatically!
What You Get for Freeβ
When you extend JpaRepository<T, ID>, you inherit:
Basic CRUD Operationsβ
// Save operations
<S extends T> S save(S entity);
<S extends T> List<S> saveAll(Iterable<S> entities);
// Find operations
Optional<T> findById(ID id);
List<T> findAll();
List<T> findAllById(Iterable<ID> ids);
// Count
long count();
// Existence check
boolean existsById(ID id);
// Delete operations
void delete(T entity);
void deleteById(ID id);
void deleteAll();
void deleteAll(Iterable<? extends T> entities);
Batch Operationsβ
// Flush changes to database
void flush();
// Save and immediately flush
<S extends T> S saveAndFlush(S entity);
// Batch delete
void deleteAllInBatch();
void deleteAllInBatch(Iterable<T> entities);
Advanced Queriesβ
// Query by example
<S extends T> List<S> findAll(Example<S> example);
// Sorting
List<T> findAll(Sort sort);
// Pagination
Page<T> findAll(Pageable pageable);
All implemented automatically - you write zero code!
Custom Queries (When Method Names Aren't Enough)β
Sometimes method names get too long or complex. Use @Query:
public interface UserRepository extends JpaRepository<User, Long> {
// Simple method name
Optional<User> findByEmail(String email); // Spring generates query
// Complex query - use @Query
@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
Optional<User> findActiveUserByEmail(@Param("email") String email);
// Native SQL
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
Optional<User> findByEmailNative(String email);
// Update query
@Modifying
@Query("UPDATE User u SET u.lastLogin = :loginTime WHERE u.id = :userId")
void updateLastLogin(@Param("userId") Long userId, @Param("loginTime") LocalDateTime loginTime);
}
Still an interface - Spring implements these too!
Common Mistakesβ
Mistake 1: Trying to Use a Classβ
// β Won't compile
public class UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Error:
JpaRepository is an interface; cannot be extended by a class
Mistake 2: Implementing the Interface Yourselfβ
// β Don't do this!
@Repository
public class UserRepositoryImpl implements UserRepository {
@Autowired
private EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
// You're writing all this manually... why?
}
// Plus you have to implement all 20+ JpaRepository methods!
}
Problem: You lose all the Spring Data magic!
Mistake 3: Annotating Interface with @Repositoryβ
// β οΈ Unnecessary (but not harmful)
@Repository // Not needed!
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Spring Data automatically detects interfaces extending JpaRepository. The @Repository annotation is redundant.
Correct:
// β
Clean - no annotation needed
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Best Practicesβ
β Do Thisβ
1. Use interfaces for repositories
public interface UserRepository extends JpaRepository<User, Long> {
// Declare methods only
}
2. Use method naming conventions
Optional<User> findByEmail(String email); // Clear and automatic
List<User> findByAgeGreaterThan(int age);
3. Use @Query for complex queries
@Query("SELECT u FROM User u WHERE u.email = :email AND u.active = true")
Optional<User> findActiveUserByEmail(@Param("email") String email);
4. Keep repositories focused
// β
Good - user-related queries only
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByActive(boolean active);
}
β Avoid Thisβ
1. Don't use classes
// β Won't work
public class UserRepository extends JpaRepository<User, Long> { }
2. Don't implement repositories manually
// β Defeats the purpose of Spring Data
public class UserRepositoryImpl implements UserRepository {
// Manual implementation...
}
3. Don't make method names too complex
// β Unreadable
Optional<User> findByEmailAndUsernameAndAgeGreaterThanAndActiveTrueOrderByCreatedAtDesc(
String email, String username, int age
);
// β
Use @Query instead
@Query("SELECT u FROM User u WHERE u.email = :email AND u.username = :username " +
"AND u.age > :age AND u.active = true ORDER BY u.createdAt DESC")
Optional<User> findActiveUserWithCriteria(@Param("email") String email,
@Param("username") String username,
@Param("age") int age);
Summaryβ
Why Interfaces?β
The declaration pattern:
You declare WHAT you want (interface)
β
Spring implements HOW to do it (proxy)
β
You get fully working repository (magic!)
Key Pointsβ
- Interfaces allow Spring to generate implementations using proxies
- Method names become queries automatically
- You get 20+ methods for free from JpaRepository
- No boilerplate code - Spring handles everything
- Type-safe - compiler checks your method signatures
The Patternβ
// Step 1: Define interface
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// Step 2: Spring generates implementation (you never see this)
// class $Proxy123 implements UserRepository { ... }
// Step 3: Use it
@Autowired
private UserRepository userRepository;
userRepository.findByEmail("user@email.com"); // β¨ Works!
Remember: In Spring Data JPA, you're declaring WHAT you want, not HOW to get it. That's why interfaces are perfect - they're declarations without implementation. Spring fills in the implementation for you! π―
Tags: #spring-boot #jpa #spring-data #repository #java #interfaces
