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.
// 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
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.
@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; } }
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 |
// 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.
// 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 } }
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.
// 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.
// ─── 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(); } }
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:
- OOP Concepts in Java
- Classes and Objects in Java
- Abstraction in Java
- Abstraction vs Encapsulation in Java
- Understanding Polymorphism in Java
- Understanding Multiple Inheritance in Java
- Generics in Java
- What is Composition in Java?
- Understanding the Java Comparator Interface
- A Quick Guide to Access Modifiers in Java
- Understanding Singleton Class in Java
- Method Overloading and Overriding in Java
- Understanding Servlets in Java
- Core Java Concepts and Syntax
- Essentials of Back-End Development: From APIs to Databases
External Resources:
- Spring Boot Official Documentation
- Spring Framework - Core: IoC Container
- Baeldung - Spring Dependency Injection Guide
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.
✓ 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.