Spring Data JPA Part 5: Why UserRepository MUST Be an Interface - Java Type System Rules
"Why can't I use class instead of interface for UserRepository?" Because Java's type system has strict rules: class extends class, interface extends interface, class implements interface. When you try class UserRepository extends JpaRepository, Java sees JpaRepository is an interface and throws an error. Let's understand the three-level inheritance chain and who implements what.
The Error That Confuses Everyoneβ
You try this:
// β Compiler error!
public class UserRepository extends JpaRepository<User, Long> {
}
Error message:
The type JpaRepository cannot be the superclass of a class
But this works:
// β
Compiles perfectly
public interface UserRepository extends JpaRepository<User, Long> {
}
Why does Java force you to use interface?
Java's Type System Rulesβ
Java has strict rules about inheritance:
| Parent Type | Child Type | Keyword | Allowed? |
|---|---|---|---|
| Class | Class | extends | β Yes |
| Interface | Interface | extends | β Yes |
| Interface | Class | implements | β Yes |
| Interface | Class | extends | β NO! |
Examplesβ
// β
Class extends Class
public class Dog extends Animal { }
// β
Interface extends Interface
public interface Runner extends Moveable { }
// β
Class implements Interface
public class Dog implements Animal { }
// β Class extends Interface - NOT ALLOWED
public class Dog extends Animal { } // Error if Animal is interface
What Happens Behind the Scenesβ
When Java Compiler Sees Your Codeβ
public class UserRepository extends JpaRepository<User, Long>
Java's thought process:
- β Check syntax:
extendskeyword looks correct - β Check: Is
UserRepositorya valid name? Yes - β οΈ Check: Is
JpaRepositorya class or interface? - β Find:
JpaRepositoryis an interface - β Error: "You can't use
extendsto inherit from an interface with a class"
The rule: extends only works when both parent and child are the same type (class-to-class OR interface-to-interface).
Why This Rule Existsβ
Java distinguishes between two levels of abstraction:
Class (Concrete Thing)β
public class Dog {
// Actual implementation code
private String color;
private int age;
public void bark() {
System.out.println("Woof!"); // Real behavior
}
}
- Has state (color, age)
- Has implementation (method bodies)
- Can be instantiated (
new Dog())
Interface (Contract/Specification)β
public interface Animal {
// Only declares what must be implemented
void makeSound(); // No body - just rules
}
- No state
- No implementation
- Just rules/contracts
- Cannot be instantiated
The logic: You can only inherit from things at the same level of abstraction.
- Class β Class (concrete from concrete) β
- Interface β Interface (contract from contract) β
- Class β Interface (concrete from abstract) β Doesn't make sense
The Three-Level Inheritance Chainβ
Level 1: JpaRepository (Interface)β
public interface JpaRepository<T, ID> {
<S extends T> S save(S entity);
Optional<T> findById(ID id);
List<T> findAll();
void delete(T entity);
// ... 30+ more methods
}
Who implements these? Nobody yet - it's just a contract.
Level 2: UserRepository (Interface)β
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
What UserRepository inherits:
- β
save()from JpaRepository - β
findById()from JpaRepository - β
delete()from JpaRepository - β ... 30+ more methods from JpaRepository
- β
Adds custom:
findByEmail()
Total methods UserRepository has: 31+ methods (30 inherited + 1 custom)
Who implements these? Still nobody - it's still just a contract.
Level 3: Spring's Proxy (Concrete Class)β
// Spring creates this automatically at runtime (you never see it)
public class UserRepository$Proxy implements UserRepository {
@Autowired
private EntityManager em;
// Spring implements all 30+ JpaRepository methods
@Override
public User save(User user) {
if (user.getId() == null) {
em.persist(user);
} else {
user = em.merge(user);
}
return user;
}
@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(em.find(User.class, id));
}
@Override
public void delete(User user) {
em.remove(em.contains(user) ? user : em.merge(user));
}
// ... 27 more JpaRepository methods implemented by Spring
// Spring implements your custom method
@Override
public Optional<User> findByEmail(String email) {
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.email = :email",
User.class
);
query.setParameter("email", email);
return query.getResultStream().findFirst();
}
}
Who implements these? Spring does - automatically, at runtime, using dynamic proxies.
Visual Inheritance Chainβ
ββββββββββββββββββββ
β JpaRepository β β Interface (30+ methods declared)
β (Interface) β
β β
β - save() β
β - findById() β
β - delete() β
β - ... 30+ more β
ββββββββββ¬ββββββββββ
β extends (interface β interface)
β
ββββββββββββββββββββββββββββ
β UserRepository β β Interface (inherits 30+, adds 1)
β (Interface) β
β β
β - save() (inherited) β
β - findById() (inherited) β
β - delete() (inherited) β
β - ... 30+ more (inherited)
β - findByEmail() (NEW) β
ββββββββββ¬ββββββββββββββββββ
β implements (interface β class)
β
ββββββββββββββββββββββββββββββββββββ
β UserRepository$Proxy β β Concrete Class (Spring creates)
β (Concrete Class) β
β β
β - save() β Spring codes β
β - findById() β Spring codes β
β - delete() β Spring codes β
β - ... 30+ more β Spring codes β
β - findByEmail() β Spring codes β
ββββββββββββββββββββββββββββββββββββ
The Confusion: "Doesn't Someone Have to Implement All 31 Methods?"β
Your Questionβ
"If a class implements
UserRepository, wouldn't it need to implement ALL 31 methods (30 from JpaRepository + 1 custom)?"
YES! Absolutely correct.
But here's the trick: You never write that class. Spring writes it for you.
What You Write (4 lines)β
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
What Spring Writes (100+ lines)β
Spring automatically generates a proxy class implementing all 31 methods:
// Generated by Spring - you never write this
public class UserRepository$Proxy implements UserRepository {
// 100+ lines of implementation code
// All 31 methods fully implemented
}
What You Useβ
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Spring injects the proxy
public User register(User user) {
return userRepository.save(user); // Calls Spring's proxy method
}
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email); // Calls Spring's proxy method
}
}
Why extends and Not implements?β
Option 1: Interface extends Interface β β
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
This says: "UserRepository IS-A JpaRepository, plus some extra methods"
- UserRepository inherits all JpaRepository methods
- UserRepository adds
findByEmail() - Total: 31+ methods declared
Option 2: Interface implements Interface ββ
// β Syntax error!
public interface UserRepository implements JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Error: implements is only for classes. Interfaces use extends.
What If You Tried to Write It Manually?β
You technically could write a concrete class:
public class UserRepositoryManual implements UserRepository {
@Autowired
private EntityManager em;
// You must implement ALL 31 methods:
@Override
public User save(User user) {
if (user.getId() == null) {
em.persist(user);
} else {
user = em.merge(user);
}
return user;
}
@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(em.find(User.class, id));
}
@Override
public List<User> findAll() {
return em.createQuery("SELECT u FROM User u", User.class)
.getResultList();
}
@Override
public void delete(User user) {
em.remove(em.contains(user) ? user : em.merge(user));
}
// ... 27 more JpaRepository methods
@Override
public Optional<User> findByEmail(String email) {
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.email = :email",
User.class
);
query.setParameter("email", email);
return query.getResultStream().findFirst();
}
}
Result: 100+ lines of boilerplate code that Spring writes for you automatically.
Spring Data's value: You declare 4 lines, Spring implements 100+ lines.
Why Spring Data Uses Interfacesβ
Spring Data's magic only works with interfaces because:
1. Dynamic Proxies Work Only with Interfacesβ
// Spring creates dynamic proxy at runtime
UserRepository proxy = ProxyFactory.createProxy(UserRepository.class);
- Java's
Proxyclass can only create proxies for interfaces - If
UserRepositorywere a concrete class, Spring couldn't wrap it
2. Interface = Contract (What), Class = Implementation (How)β
// Interface: "What should happen"
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); // What: find user by email
}
// Spring's Proxy: "How it happens"
public class UserRepository$Proxy implements UserRepository {
public Optional<User> findByEmail(String email) {
// How: Generate SQL SELECT WHERE email = ?
return executeQuery("SELECT * FROM users WHERE email = ?", email);
}
}
The Logic Flowβ
Step-by-step what happens:
-
You declare interface:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
} -
Spring scans and finds it:
- "This interface extends
JpaRepository" - "So it's a repository interface"
- "This interface extends
-
Spring creates dynamic proxy:
UserRepository$Proxy proxy = new UserRepository$Proxy();
// Implements all 31 methods -
Spring registers proxy as bean:
@Bean
public UserRepository userRepository() {
return proxy;
} -
Spring injects proxy into your service:
@Autowired
private UserRepository userRepository; // Gets the proxy -
You call methods:
userRepository.findByEmail("test@example.com"); -
Proxy intercepts and executes:
- Proxy generates SQL:
SELECT * FROM users WHERE email = ? - Proxy executes query
- Proxy returns result
- Proxy generates SQL:
Comparison Tableβ
| Concept | What It Is | Who Implements |
|---|---|---|
| JpaRepository | Interface with 30+ methods | (abstract - no implementation) |
| UserRepository extends JpaRepository | Interface inheriting 30+ methods + adding findByEmail() | (abstract - no implementation) |
| UserRepository$Proxy implements UserRepository | Concrete class implementing all 31 methods | Spring (automatically) |
Summaryβ
The Java Type System Ruleβ
You can't do:
public class UserRepository extends JpaRepository // β Error
Because:
JpaRepositoryis an interfaceUserRepositorywould be a class- Java doesn't allow class extends interface
You must do:
public interface UserRepository extends JpaRepository // β
Correct
Because:
JpaRepositoryis an interfaceUserRepositoryis an interface- Java allows interface extends interface
The Implementation Chainβ
- JpaRepository (interface) β Declares 30+ methods
- UserRepository (interface) β Extends JpaRepository, adds
findByEmail() - UserRepository$Proxy (class) β Spring implements all 31 methods
The Genius of Spring Dataβ
- You write: 4 lines (interface declaration)
- Spring writes: 100+ lines (proxy implementation)
- You use: The proxy (injected automatically)
The type system forces you to use interfaces, which enables Spring to use dynamic proxies, which is exactly the mechanism that makes the magic work.
Best Practicesβ
β DO: Keep repositories as interfacesβ
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
β DON'T: Try to make repositories concrete classesβ
// β Won't compile
public class UserRepository extends JpaRepository<User, Long> {
}
// β Would compile but defeats the purpose
public class UserRepositoryImpl implements UserRepository {
// Now you have to write 100+ lines of boilerplate
}
β DO: Trust Spring to implement methodsβ
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Spring's proxy injected
public User register(User user) {
return userRepository.save(user); // Works automatically
}
}
Now you understand why the Java compiler forces you to use interface - it's not arbitrary, it's a fundamental type system rule that enables the entire Spring Data JPA magic through dynamic proxies!
