Java OOP Concepts Every Spring Developer Must Master (With Real Code Examples)

Java OOP Concepts Every Spring Developer Must Master (With Real Code Examples)

Spring Boot is often described as a framework. But at its core, it is an OOP framework — one that uses Java's object-oriented features so thoroughly that understanding Spring without understanding OOP is like reading a sentence without knowing the alphabet.

Every @Service annotation is Spring managing an object. Every @Autowired injection relies on interfaces and polymorphism. Every @Entity class uses encapsulation. Every JpaRepository extension uses inheritance. Spring didn't invent these patterns — it built an entire ecosystem on top of Java's OOP foundation.

This blog walks through the five OOP concepts Spring Boot uses most heavily, shows exactly where each appears in real Spring code, and explains why understanding the Java underneath makes you a dramatically better Spring developer. If you want a solid foundation before diving in, Board Infinity's guide on OOP concepts in Java is the perfect starting point.

Who This Blog Is For

This blog is for you if:

  • You know Spring Boot basics but feel like you're guessing more than understanding
  • You can copy Spring annotations but can't explain what they're doing under the hood
  • You want to debug Spring issues confidently instead of searching Stack Overflow for every error
  • You're building your Java foundation before or alongside your Spring Boot journey

1. Classes and Objects — The Blueprint of Every Spring Component

Everything in Spring Boot is a class. Your controllers are classes. Your services are classes. Your entities are classes. Spring doesn't create anything from scratch — it takes your Java classes and manages them as objects called beans.

When Spring sees @Component, @Service, or @Repository on a class, it does exactly what Java's new keyword does: it instantiates the class as an object and registers it in the application context. The difference is that Spring manages the lifecycle — creation, injection, and destruction — so you don't have to.

Understanding classes and objects in Java means understanding what Spring is actually doing when it "auto-configures" your application. It's not magic — it's Java object creation at scale.

Java — What Spring Does With Your @Service Class
// You write this — a plain Java class with an annotation
@Service
public class OrderService {
private final OrderRepository orderRepository;

// Spring calls this constructor automatically (constructor injection)
public OrderService(OrderRepository orderRepository) {
    this.orderRepository = orderRepository;
}

public Order placeOrder(OrderRequest request) {
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setTotal(request.getTotal());
    order.setStatus("PENDING");
    return orderRepository.save(order);
}
}
// Spring does this internally at startup — equivalent to:
OrderService orderService = new OrderService(orderRepository);
// Then stores it and injects it wherever @Autowired or constructor injection is used
💡
Spring Creates Beans — You Write Classes

You never call new OrderService() in a Spring app. Spring reads your @Service annotation and instantiates the class for you at startup. This is called Inversion of Control (IoC) — and it's 100% powered by Java class instantiation under the hood.

2. Encapsulation — How Spring Beans Protect Their State

Encapsulation means keeping a class's internal data private and exposing only what's necessary through controlled methods. In Spring Boot, this principle appears in every @Entity class, every DTO, and every service class you write.

The most important place encapsulation matters in Spring is @Entity classes. When Hibernate maps your Java class to a database table, it uses the getter and setter methods — not direct field access. If your fields are public, you've broken encapsulation and also opened your data layer to uncontrolled modification.

There's a subtler reason too: Spring Security stores sensitive data like passwords in User entities. Encapsulation ensures you never accidentally expose a raw password through a getter. Read more about this principle in Board Infinity's post on abstraction and encapsulation in Java.

Java — Encapsulation in a Spring @Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;         // private — access controlled

private String email;    // private — exposed only via getter
private String password; // private — NO getter exposed
private boolean active;

// Controlled access — Hibernate uses these for mapping
public Long    getId()      { return id; }
public String  getEmail()   { return email; }
public boolean isActive()   { return active; }

public void setEmail(String email)    { this.email = email; }
public void setActive(boolean active) { this.active = active; }

// Password setter exists — but NO getPassword()
// Spring Security's PasswordEncoder handles password access
public void setPassword(String password) { this.password = password; }
}
⚠️
Never Add getPassword() to Your User Entity

A common beginner mistake is adding a getPassword() method to the User entity for convenience. This exposes raw (or even hashed) passwords in API responses if you're not careful with your DTO mapping. Spring Security's PasswordEncoder handles password comparison — your entity never needs to return it.

3. Inheritance vs Composition — What Spring Actually Prefers

Inheritance lets a class acquire the properties and methods of another class. In Spring Boot, you use inheritance every single time you write a repository. UserRepository extends JpaRepository<User, Long> — that extends keyword is inheritance, and it gives your repository 20+ methods (findAll, findById, save, delete, count, and more) without writing a single line of implementation.

However, Spring as a framework strongly prefers composition over inheritance for your own business logic. Rather than building deep class hierarchies, Spring encourages injecting dependencies as collaborators. This is the entire point of Dependency Injection — compose objects from smaller pieces rather than inheriting from large base classes.

Understanding the difference between inheritance in Java and composition is what allows you to make the right design choice in Spring applications.

Approach When Spring Uses It Example in Spring Your Code Should Use
Inheritance Repositories, framework extension points extends JpaRepository Rarely — only when extending Spring's own classes
Composition Services using repositories, controllers using services @Autowired UserService Always — inject dependencies, don't inherit them
Interface Service contracts, Spring Security, Spring Data implements UserDetails Often — define contracts, inject implementations
Java — Inheritance for Repositories, Composition for Services
// INHERITANCE — extends gives you 20+ methods for free
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    // findById, findAll, save, delete — all inherited from JpaRepository
}
// COMPOSITION — inject dependencies, don't inherit them
@Service
public class OrderService {
// Composed with UserRepository and EmailService — not inheriting them
private final UserRepository userRepository;
private final EmailService   emailService;

public OrderService(UserRepository userRepo, EmailService emailSvc) {
    this.userRepository = userRepo;
    this.emailService   = emailSvc;
}

public void confirmOrder(Long userId, Order order) {
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException(userId));
    emailService.sendConfirmation(user.getEmail(), order);
}
}

4. Interfaces and Abstraction - The Foundation of Spring's @Service Pattern

Interfaces are the most important OOP concept for Spring Boot developers. Spring's entire dependency injection system is built around injecting interfaces, not concrete classes. This is what makes Spring applications testable, flexible, and maintainable.

The pattern is simple: define an interface that declares what a service does, create a class that implements it, and let Spring inject the interface wherever it's needed. The controller never knows which implementation it's using - it only knows the contract.

Abstraction takes this further. An abstract class can define shared behavior that multiple subclasses extend, while leaving specific methods to be implemented by each subclass. Spring uses this in test configurations, security adapters, and web configuration classes.

Understanding abstraction in Java and how it differs from interfaces is what lets you read Spring's own source code without confusion.

Java — Interface + Implementation Pattern in Spring Boot
// Step 1: Define the interface — the contract
public interface NotificationService {
    void send(String recipient, String message);
    boolean isAvailable();
}
// Step 2: Email implementation
@Service
@Primary // Spring uses this by default when multiple implementations exist
public class EmailNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
    // Send via email provider
}

@Override
public boolean isAvailable() { return true; }
}
// Step 3: SMS implementation
@Service
@Qualifier("sms")
public class SmsNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
    // Send via SMS gateway
}

@Override
public boolean isAvailable() { return true; }
}
// Step 4: Controller injects the INTERFACE — not a concrete class
@RestController
public class AlertController {
private final NotificationService notificationService; // interface type

public AlertController(NotificationService notificationService) {
    this.notificationService = notificationService;
    // Spring injects EmailNotificationService (marked @Primary)
    // Swap to SmsNotificationService with @Qualifier("sms") — zero code change
}
}
🔍
Always Inject the Interface — Not the Concrete Class

When declaring a dependency in Spring, always use the interface type — not the implementation class. Write private final NotificationService notificationService — NOT private final EmailNotificationService emailNotificationService. This keeps your code loosely coupled, testable, and easy to swap. If you only have one implementation, Spring will inject it automatically without any extra configuration.

5. Polymorphism - How Spring Picks the Right Bean

Polymorphism means one interface, many behaviours. In Spring, this is exactly how the injection system works. When multiple beans implement the same interface, Spring uses polymorphism to decide which one to inject - guided by @Primary, @Qualifier, or Spring profiles.

This is also the mechanism behind Spring's own internal components. HandlerMapping, ViewResolver, MessageConverter - all are interfaces with multiple implementations. Spring selects the right one at runtime based on your configuration. Understanding polymorphism in Java makes reading Spring's own architecture genuinely enjoyable rather than mysterious.

The practical benefit for you: once you understand polymorphism, you can swap implementations (switch from in-memory storage to a database, or switch from email to SMS notifications) with a single annotation change and zero business logic changes.

Java — Polymorphism Controlling Spring Bean Selection
// Same interface — two different behaviours
public interface PaymentGateway {
    PaymentResult process(PaymentRequest request);
}
@Service
@Profile("production") // Active in production environment
public class StripeGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) {
// Real Stripe API call
return stripeClient.charge(request);
}
}
@Service
@Profile("development") // Active in dev — no real charges
public class MockPaymentGateway implements PaymentGateway {
@Override
public PaymentResult process(PaymentRequest request) {
return PaymentResult.success("mock-tx-id-12345");
}
}
// Service uses the interface — Spring injects the right one per environment
@Service
public class CheckoutService {
private final PaymentGateway paymentGateway; // polymorphic reference

public CheckoutService(PaymentGateway paymentGateway) {
    this.paymentGateway = paymentGateway;
}

public Order checkout(Cart cart) {
    PaymentResult result = paymentGateway.process(cart.toPaymentRequest());
    // Works identically in dev (mock) and production (Stripe)
    return orderService.createFromCart(cart, result);
}
}

All 5 OOP Concepts in One Real Spring Application

Let's bring all five concepts together in a single cohesive example - a User management feature that touches every layer of a Spring Boot application.

Java — All 5 OOP Concepts in One Spring Boot Feature
// ─── ENCAPSULATION: Entity with private fields ────────────────
@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String role;
public Long   getId()    { return id; }
public String getEmail() { return email; }
public String getRole()  { return role; }
public void   setEmail(String e) { this.email = e; }
public void   setRole(String r)  { this.role = r; }
}
// ─── INHERITANCE: Repository inherits 20+ methods ─────────────
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// ─── ABSTRACTION: Interface defines the contract ───────────────
public interface UserService {
User       createUser(String email, String role);
User       findByEmail(String email);
List<User> getAllUsers();
}
// ─── CLASSES + COMPOSITION: Service implements & composes ──────
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepo; // composed — not inherited

public UserServiceImpl(UserRepository userRepo) {
    this.userRepo = userRepo;
}

@Override
public User createUser(String email, String role) {
    User user = new User();
    user.setEmail(email);
    user.setRole(role);
    return userRepo.save(user);
}

@Override
public User findByEmail(String email) {
    return userRepo.findByEmail(email)
        .orElseThrow(() -> new RuntimeException("User not found"));
}

@Override
public List<User> getAllUsers() { return userRepo.findAll(); }
}
// ─── POLYMORPHISM: Controller uses interface, not impl ─────────
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService; // polymorphic — interface type

public UserController(UserService userService) {
    this.userService = userService;
    // Spring injects UserServiceImpl — controller doesn't know or care
}

@PostMapping
public ResponseEntity<User> create(@RequestBody CreateUserRequest req) {
    return ResponseEntity.ok(userService.createUser(req.getEmail(), req.getRole()));
}

@GetMapping
public List<User> getAll() { return userService.getAllUsers(); }
}
⚠️
Don't Skip the Interface — Even for Single Implementations

Beginners often skip creating the UserService interface when there's only one implementation, thinking it's unnecessary boilerplate. Don't. The interface is what makes your code testable — in unit tests, you can mock the interface with @MockBean without loading the full Spring context. This single habit will save you hours of test setup pain later.

OOP Concept → Spring Usage - Quick Reference

OOP Concept Where It Appears in Spring Spring Annotation / Feature What Breaks Without It
Classes & Objects Every Spring component @Service, @Controller, @Repository No mental model for what Spring manages
Encapsulation Entity classes, DTOs, service state @Entity, private fields + getters Data leaks, broken Hibernate mappings, exposed passwords
Inheritance Repositories, framework extension extends JpaRepository Can't understand which methods are available
Interfaces & Abstraction Service contracts, Spring Security, Spring Data implements UserDetails, @Primary Untestable code, tightly coupled layers
Polymorphism Bean selection, environment switching @Qualifier, @Profile, @Primary Can't swap implementations, rigid architecture

Further Reading

Board Infinity Guides:

External Resources:

🚀 Learn Java OOP for Spring — Hands On

Java Programming Fundamentals for Spring Boot Development

This free Coursera course by Board Infinity teaches exactly the Java OOP concepts covered in this blog — applied directly to Spring Boot, Spring MVC, and Spring Security development. Every lesson is practical, every example connects to real Spring code.

Module 1
Java Basics & Programming Foundations Platform fundamentals, syntax, data types, operators, and control flow
Module 2
Object-Oriented Thinking in Java Classes, encapsulation, inheritance, abstraction, interfaces — all five OOP concepts from this blog, hands on
Module 3
Java Essentials Used in Spring Exception handling, collections (List, Set, Map), generics, and type safety
Module 4
Modern Java Features for Spring Lambda expressions, Streams, Optional, annotations, and reflection basics
Start Learning on Coursera →

✓ Certificate available  ·  ✓ Self-paced  ·  ✓ Beginner-friendly

Conclusion

OOP is not background theory for Spring developers - it is the operating system that Spring runs on. Every annotation, every injection, every repository method exists because of a Java OOP concept underneath it.

The five concepts covered here - Classes & Objects, Encapsulation, Inheritance, Interfaces & Abstraction, and Polymorphism - are not a comprehensive tour of Java OOP. They are the specific subset that Spring Boot uses daily, in every layer, on every request. Master these and Spring stops feeling like magic. It starts feeling like well-organized Java.

The best Spring Boot developers are not the ones who memorised the most annotations. They're the ones who understand the Java underneath the annotations. That understanding starts with OOP - and it compounds into faster debugging, cleaner architecture, and code you're genuinely proud of.

Programming Language Java Software Development Featured Posts Programming