Classes vs Interfaces in Java: The Deep Dive (Part 2)
In Part 1, we learned why Spring Data JPA repositories must be interfaces. Now let's understand the fundamental difference between classes and interfaces in Java, and when to use each.
Core Definitionsβ
Interface = A Contract (WHAT)β
An interface defines what something must do, not how.
public interface PaymentProcessor {
boolean processPayment(double amount);
String getTransactionId();
// No code here - just the promise of what must exist
}
Key characteristics:
- β Cannot be instantiated (
new PaymentProcessor()won't work) - β Only defines method signatures (no implementation)
- β A contract that says: "Whoever implements me must provide these methods"
- π‘ Think: "A menu that lists what's available"
Class = An Implementation (HOW)β
A class contains actual code that does something.
public class CreditCardProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
// ACTUAL CODE that does something
validateCard();
chargeCard(amount);
return true;
}
@Override
public String getTransactionId() {
return "CC_" + System.currentTimeMillis();
}
// Private helper methods with actual logic
private void validateCard() {
// Validation code here
}
private void chargeCard(double amount) {
// Charging code here
}
}
Key characteristics:
- β Contains actual implementation code
- β
Can be instantiated (
new CreditCardProcessor()) - β Can extend another class (single inheritance)
- β Can have state (fields/variables)
- π‘ Think: "A chef who actually makes the dishes"
The Restaurant Analogy (Extended)β
The Menu (Interface)β
public interface RestaurantMenu {
Dish serveBreakfast();
Dish serveLunch();
Dish serveDinner();
}
What a menu does:
- Lists what dishes are available
- Doesn't tell you HOW to cook
- Different restaurants implement the same menu differently
The Chef (Class)β
public class ItalianChef implements RestaurantMenu {
@Override
public Dish serveBreakfast() {
// Italian breakfast implementation
return new Dish("Cornetto and espresso");
}
@Override
public Dish serveLunch() {
return new Dish("Pasta carbonara");
}
@Override
public Dish serveDinner() {
return new Dish("Osso buco");
}
}
public class JapaneseChef implements RestaurantMenu {
@Override
public Dish serveBreakfast() {
// Japanese breakfast implementation
return new Dish("Miso soup and rice");
}
@Override
public Dish serveLunch() {
return new Dish("Sushi");
}
@Override
public Dish serveDinner() {
return new Dish("Ramen");
}
}
Same menu (interface), different implementations (classes)!
Why Spring Data Uses Interfacesβ
Reason 1: Proxy Generationβ
Your code:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
}
What Spring generates (simplified):
// You never see this - Spring creates it at runtime
public class UserRepositoryImpl implements UserRepository {
@Autowired
private EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
// Spring generates SQL: SELECT * FROM users WHERE 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 SQL: SELECT * FROM users WHERE username = ?
TypedQuery<User> query = entityManager.createQuery(
"SELECT u FROM User u WHERE u.username = :username", User.class
);
query.setParameter("username", username);
return query.getResultStream().findFirst();
}
// Plus all 20+ methods from JpaRepository:
// save(), findById(), findAll(), delete(), count(), etc.
// Spring implements ALL of these for you!
}
The magic: You declare WHAT you want, Spring implements HOW to get it.
Reason 2: Flexibilityβ
Multiple implementations can exist:
// Production: Real database
public class UserRepositoryJpaImpl implements UserRepository {
// Uses actual database
}
// Testing: In-memory mock
public class UserRepositoryMockImpl implements UserRepository {
private Map<Long, User> users = new HashMap<>();
@Override
public Optional<User> findByEmail(String email) {
return users.values().stream()
.filter(u -> u.getEmail().equals(email))
.findFirst();
}
}
// Testing: Fake data
public class UserRepositoryFakeImpl implements UserRepository {
@Override
public Optional<User> findByEmail(String email) {
return Optional.of(new User("test@email.com"));
}
}
Same interface, different behaviors!
Reason 3: Contractsβ
The interface is a promise:
public interface EmailService {
void sendWelcomeEmail(User user);
void sendPasswordResetEmail(User user);
void sendOrderConfirmation(User user, Order order);
}
Any class implementing this MUST provide all three methods:
// SMTP implementation
public class SmtpEmailService implements EmailService {
@Override
public void sendWelcomeEmail(User user) {
// Send via SMTP server
}
@Override
public void sendPasswordResetEmail(User user) {
// Send via SMTP server
}
@Override
public void sendOrderConfirmation(User user, Order order) {
// Send via SMTP server
}
}
// SendGrid implementation
public class SendGridEmailService implements EmailService {
@Override
public void sendWelcomeEmail(User user) {
// Send via SendGrid API
}
@Override
public void sendPasswordResetEmail(User user) {
// Send via SendGrid API
}
@Override
public void sendOrderConfirmation(User user, Order order) {
// Send via SendGrid API
}
}
// Testing: Mock that doesn't actually send
public class MockEmailService implements EmailService {
@Override
public void sendWelcomeEmail(User user) {
System.out.println("Would send welcome email to " + user.getEmail());
}
@Override
public void sendPasswordResetEmail(User user) {
System.out.println("Would send password reset to " + user.getEmail());
}
@Override
public void sendOrderConfirmation(User user, Order order) {
System.out.println("Would send order confirmation to " + user.getEmail());
}
}
Reason 4: No Boilerplateβ
Without Spring Data (manual class):
@Repository
public class UserRepositoryManual {
@Autowired
private EntityManager em;
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();
}
public Optional<User> findByUsername(String username) {
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.username = :username", User.class
);
query.setParameter("username", username);
return query.getResultStream().findFirst();
}
public User save(User user) {
if (user.getId() == null) {
em.persist(user);
return user;
} else {
return em.merge(user);
}
}
public Optional<User> findById(Long id) {
return Optional.ofNullable(em.find(User.class, id));
}
public List<User> findAll() {
return em.createQuery("SELECT u FROM User u", User.class).getResultList();
}
public void delete(User user) {
em.remove(em.contains(user) ? user : em.merge(user));
}
public long count() {
return em.createQuery("SELECT COUNT(u) FROM User u", Long.class).getSingleResult();
}
// ... and 20+ more methods!
}
With Spring Data (interface):
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
// That's it! All other methods inherited and implemented automatically!
}
Lines of code: 100+ vs 4!
When to Use Eachβ
Use Interface When:β
1. You Want to Define a Contractβ
// Contract for payment processing
public interface PaymentProcessor {
boolean processPayment(double amount);
String getTransactionId();
}
// Contract for data access
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// Contract for business logic
public interface UserService {
User registerUser(String email, String password);
User login(String email, String password);
}
2. Multiple Implementations Exist or Might Existβ
public interface NotificationService {
void notify(User user, String message);
}
// Email implementation
public class EmailNotificationService implements NotificationService {
@Override
public void notify(User user, String message) {
sendEmail(user.getEmail(), message);
}
}
// SMS implementation
public class SmsNotificationService implements NotificationService {
@Override
public void notify(User user, String message) {
sendSms(user.getPhone(), message);
}
}
// Push notification implementation
public class PushNotificationService implements NotificationService {
@Override
public void notify(User user, String message) {
sendPushNotification(user.getDeviceToken(), message);
}
}
3. You Need Flexibility and Loose Couplingβ
@Service
public class OrderService {
@Autowired
private NotificationService notificationService; // Interface!
public void placeOrder(Order order) {
// Process order...
// Spring can inject ANY implementation:
// - EmailNotificationService
// - SmsNotificationService
// - PushNotificationService
// - MockNotificationService (for testing)
notificationService.notify(order.getUser(), "Order placed!");
}
}
4. Using Frameworks That Generate Implementationsβ
// Spring Data JPA generates implementation
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategory(String category);
}
// Spring generates the SQL and implementation automatically!
Use Class When:β
1. You Need Actual Implementation Codeβ
@Entity
public class User {
// Actual data storage
private Long id;
private String email;
private String password;
// Actual behavior
public boolean checkPassword(String password) {
return BCrypt.checkpw(password, this.password);
}
}
2. You Want to Instantiate It Directlyβ
// You can create instances
User user = new User("john@email.com");
Product product = new Product("Laptop", 999.99);
Order order = new Order(user, product);
3. State/Data Belongs to a Single Thingβ
public class ShoppingCart {
// State (data)
private List<CartItem> items = new ArrayList<>();
private User user;
private double totalAmount;
// Behavior operating on that state
public void addItem(Product product, int quantity) {
items.add(new CartItem(product, quantity));
calculateTotal();
}
public void removeItem(Product product) {
items.removeIf(item -> item.getProduct().equals(product));
calculateTotal();
}
private void calculateTotal() {
totalAmount = items.stream()
.mapToDouble(item -> item.getProduct().getPrice() * item.getQuantity())
.sum();
}
}
4. It's Concrete and Specificβ
public class EmailValidator {
// Utility class with specific implementation
public static boolean isValid(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
}
public class PasswordHasher {
// Specific implementation for password hashing
public static String hash(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
}
}
Java Type System Overviewβ
Not Everything is a Class!β
Java has several key type concepts:
1. Classes (Concrete Implementations)β
@Entity
public class User {
private Long id;
private String email;
}
@Service
public class UserServiceImpl implements UserService {
// Business logic implementation
}
Purpose: Store data and implement behavior
2. Interfaces (Contracts)β
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
public interface PaymentProcessor {
boolean processPayment(double amount);
}
Purpose: Define contracts without implementation
3. Abstract Classes (Partial Implementations)β
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Common behavior for all entities
public abstract void validate();
}
@Entity
public class User extends BaseEntity {
private String email;
@Override
public void validate() {
if (email == null) throw new IllegalStateException("Email required");
}
}
Purpose: Provide partial implementation, force subclasses to implement specific methods
Rarely used in Spring apps - interfaces are preferred for flexibility
4. Enums (Fixed Set of Constants)β
public enum Role {
USER,
ADMIN,
MODERATOR
}
public enum OrderStatus {
PENDING,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED
}
Purpose: Type-safe constants
Analyzing Your E-Commerce Appβ
Question 1: Why is User a class?β
@Entity
public class User {
@Id
private Long id;
private String username;
private String email;
private String password;
// Getters/setters
}
Answer:
- β Represents a concrete entity with state (id, email, password)
- β
You instantiate it:
new User() - β Has actual data to store in database
- β Needs concrete implementation of getters/setters
Could it be an interface? No! Interfaces can't store data.
Question 2: Why is UserRepository an interface?β
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
}
Answer:
- β It's a contract saying "provide these data access methods"
- β Spring implements it automatically (you don't write SQL!)
- β Flexibility: Could swap implementations (testing vs production)
- β No boilerplate: Spring generates all the code
Could it be a class? Technically no (JpaRepository is interface), but even if it could, you'd lose Spring's automatic implementation!
Question 3: Why is UserServiceImpl a class?β
public interface UserService {
User registerUser(String email, String password);
User login(String email, String password);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User registerUser(String email, String password) {
// Actual implementation with business logic
User user = new User();
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
return userRepository.save(user);
}
@Override
public User login(String email, String password) {
// Actual implementation
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException("User not found"));
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
return user;
}
}
Answer:
- β
Implements
UserServiceinterface (the contract) - β Contains actual business logic (how to register/login)
- β Gets instantiated by Spring and injected as dependency
- β Concrete implementation that can be tested/mocked
The pattern:
UserService (interface) = "What operations are available"
UserServiceImpl (class) = "How to actually do those operations"
The Key Insightβ
Classes = Implementation (HOW)β
"Here's the code that actually does the work"
public class Calculator {
public int add(int a, int b) {
return a + b; // β Actual implementation
}
}
// You write the methods with actual logic
Interfaces = Contract (WHAT)β
"Here's what capabilities you must provide"
public interface Calculator {
int add(int a, int b); // β Just the promise
}
// You declare methods without implementation
// Multiple classes can implement this differently
In Spring Dataβ
You write: INTERFACE (the contract)
β
Spring writes: IMPLEMENTATION (the how)
β
You get: Fully working repository with zero SQL!
This saves you from:
- Writing SQL queries
- Writing EntityManager code
- Writing transaction management
- Writing exception handling
- Writing 20+ boilerplate methods
You just declare what you want, Spring provides it!
Practical Examplesβ
Example 1: Payment Processingβ
Interface (Contract):
public interface PaymentProcessor {
PaymentResult process(Order order);
void refund(String transactionId);
}
Multiple Implementations:
public class StripeProcessor implements PaymentProcessor {
@Override
public PaymentResult process(Order order) {
// Stripe API implementation
return stripeAPI.charge(order.getTotal());
}
@Override
public void refund(String transactionId) {
stripeAPI.refund(transactionId);
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public PaymentResult process(Order order) {
// PayPal API implementation
return paypalAPI.charge(order.getTotal());
}
@Override
public void refund(String transactionId) {
paypalAPI.refund(transactionId);
}
}
Usage (Flexible):
@Service
public class OrderService {
@Autowired
private PaymentProcessor paymentProcessor; // Interface!
public void checkout(Order order) {
// Spring can inject ANY implementation:
// - StripeProcessor
// - PayPalProcessor
// - MockProcessor (testing)
PaymentResult result = paymentProcessor.process(order);
}
}
Example 2: Notification Systemβ
Interface:
public interface NotificationSender {
void send(User user, String message);
}
Implementations:
@Component("email")
public class EmailSender implements NotificationSender {
@Override
public void send(User user, String message) {
// Email implementation
}
}
@Component("sms")
public class SmsSender implements NotificationSender {
@Override
public void send(User user, String message) {
// SMS implementation
}
}
@Component("push")
public class PushSender implements NotificationSender {
@Override
public void send(User user, String message) {
// Push notification implementation
}
}
Dynamic Selection:
@Service
public class NotificationService {
@Autowired
private Map<String, NotificationSender> senders; // All implementations!
public void notifyUser(User user, String message, String method) {
NotificationSender sender = senders.get(method); // Get by name
sender.send(user, message);
}
}
// Usage:
notificationService.notifyUser(user, "Welcome!", "email");
notificationService.notifyUser(user, "Order shipped!", "sms");
notificationService.notifyUser(user, "Sale alert!", "push");
Summaryβ
The Big Pictureβ
| Aspect | Interface | Class |
|---|---|---|
| What it is | Contract | Implementation |
| Contains | Method signatures only | Actual code |
| Can instantiate? | β No | β Yes |
| Can have state? | β No | β Yes |
| Multiple inheritance? | β Yes (implements many) | β No (extends one) |
| Use for | Defining contracts | Providing implementations |
| Spring Data | Repository interfaces | Entity classes |
When to Use Whatβ
Interface:
- Defining contracts
- Spring Data repositories
- Service layer contracts
- Multiple implementations needed
- Flexibility required
Class:
- Entity objects (User, Product, Order)
- Service implementations (UserServiceImpl)
- Utility classes (DateUtil, Logger)
- Data transfer objects (DTOs)
- Request/Response objects
The Spring Data Patternβ
// 1. Define interface (contract)
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// 2. Spring generates implementation (you never see this)
// class $Proxy123 implements UserRepository { ... }
// 3. Use it like magic!
@Autowired
private UserRepository userRepository;
userRepository.findByEmail("user@email.com"); // β¨ Just works!
Key Takeaway: Interfaces define WHAT (the contract), classes define HOW (the implementation). Spring Data JPA uses interfaces because it wants you to declare what operations you need, then it implements them for you automatically! π―
Tags: #java #interfaces #classes #spring-boot #jpa #fundamentals #oop
