Skip to main content

Spring Data JPA Part 5: Why UserRepository MUST Be an Interface - Java Type System Rules

Β· 8 min read
Mahmut Salman
Software Developer

"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 TypeChild TypeKeywordAllowed?
ClassClassextendsβœ… Yes
InterfaceInterfaceextendsβœ… Yes
InterfaceClassimplementsβœ… Yes
InterfaceClassextends❌ 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:

  1. βœ“ Check syntax: extends keyword looks correct
  2. βœ“ Check: Is UserRepository a valid name? Yes
  3. ⚠️ Check: Is JpaRepository a class or interface?
  4. ❌ Find: JpaRepository is an interface
  5. ❌ Error: "You can't use extends to 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 Proxy class can only create proxies for interfaces
  • If UserRepository were 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:

  1. You declare interface:

    public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    }
  2. Spring scans and finds it:

    • "This interface extends JpaRepository"
    • "So it's a repository interface"
  3. Spring creates dynamic proxy:

    UserRepository$Proxy proxy = new UserRepository$Proxy();
    // Implements all 31 methods
  4. Spring registers proxy as bean:

    @Bean
    public UserRepository userRepository() {
    return proxy;
    }
  5. Spring injects proxy into your service:

    @Autowired
    private UserRepository userRepository; // Gets the proxy
  6. You call methods:

    userRepository.findByEmail("test@example.com");
  7. Proxy intercepts and executes:

    • Proxy generates SQL: SELECT * FROM users WHERE email = ?
    • Proxy executes query
    • Proxy returns result

Comparison Table​

ConceptWhat It IsWho Implements
JpaRepositoryInterface with 30+ methods(abstract - no implementation)
UserRepository extends JpaRepositoryInterface inheriting 30+ methods + adding findByEmail()(abstract - no implementation)
UserRepository$Proxy implements UserRepositoryConcrete class implementing all 31 methodsSpring (automatically)

Summary​

The Java Type System Rule​

You can't do:

public class UserRepository extends JpaRepository  // ❌ Error

Because:

  • JpaRepository is an interface
  • UserRepository would be a class
  • Java doesn't allow class extends interface

You must do:

public interface UserRepository extends JpaRepository  // βœ… Correct

Because:

  • JpaRepository is an interface
  • UserRepository is an interface
  • Java allows interface extends interface

The Implementation Chain​

  1. JpaRepository (interface) β†’ Declares 30+ methods
  2. UserRepository (interface) β†’ Extends JpaRepository, adds findByEmail()
  3. 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!