Skip to main content

Why Spring Data JPA Repositories Must Be Interfaces, Not Classes

Β· 10 min read
Mahmut Salman
Software Developer

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 NameGenerated 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​

  1. Interfaces allow Spring to generate implementations using proxies
  2. Method names become queries automatically
  3. You get 20+ methods for free from JpaRepository
  4. No boilerplate code - Spring handles everything
  5. 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