Learn what Dependency Injection is, why it matters in Spring Boot, and how to implement it using Java 21 with practical examples, REST APIs, and best practices.
What is Dependency Injection & How is it Achieved in Spring Boot?
Introduction
Imagine you're building a house.
You need electricity, plumbing, internet, and furniture. Now imagine if the house itself had to create all these things from scratch. It would become extremely complex and difficult to maintain.
Instead, specialized providers supply these services, and the house simply uses them.
Dependency Injection (DI) works in a similar way.
In traditional Java programming, a class often creates the objects it depends on using the new keyword. This creates tight coupling, making the application harder to test, maintain, and extend.
With Dependency Injection, the required objects (dependencies) are provided to a class from the outside instead of being created inside it.
This concept is at the heart of Spring Boot and is one of the primary reasons why Spring Boot applications are flexible, scalable, and easy to test.
In this article, you'll learn:
- What Dependency Injection is
- Why it is important
- How Spring Boot implements it
- Practical Java 21 examples
- Best practices and common mistakes
Core Concepts
What is Dependency Injection?
Dependency Injection is a design pattern where an object's dependencies are supplied externally rather than being created by the object itself.
Without Dependency Injection
public class OrderService {
private PaymentService paymentService = new PaymentService();
}
The OrderService is responsible for creating PaymentService.
Problems:
- Tight coupling
- Difficult unit testing
- Hard to replace implementations
- Violates the Dependency Inversion Principle
With Dependency Injection
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
Now the dependency is provided from outside.
Benefits:
- Loose coupling
- Better testability
- Easier maintenance
- More flexible architecture
How Spring Boot Achieves Dependency Injection
Spring Boot uses the Spring IoC (Inversion of Control) Container.
The container:
- Creates objects (Beans)
- Manages their lifecycle
- Injects dependencies automatically
Common annotations used:
| Annotation | Purpose |
|---|---|
@Component |
Generic Spring Bean |
@Service |
Service layer Bean |
@Repository |
Data access Bean |
@Controller |
MVC Controller |
@RestController |
REST API Controller |
@Autowired |
Inject dependency |
@Configuration |
Bean configuration |
@Bean |
Creates custom Bean |
Types of Dependency Injection in Spring Boot
1. Constructor Injection (Recommended)
public UserService(UserRepository repository) {
this.repository = repository;
}
Advantages:
- Immutable dependencies
- Easier testing
- Clear design
- Recommended by Spring Team
2. Setter Injection
public void setRepository(UserRepository repository) {
this.repository = repository;
}
Useful for optional dependencies.
3. Field Injection
@Autowired
private UserRepository repository;
Not recommended because:
- Harder to test
- Hidden dependencies
- Reduced immutability
Use Cases of Dependency Injection
Dependency Injection is commonly used in:
REST APIs
Inject services into controllers.
Database Access
Inject repositories into services.
Messaging Systems
Inject Kafka or RabbitMQ producers.
Payment Gateways
Switch between different payment providers easily.
Unit Testing
Replace real objects with mocks.
Code Example 1: Constructor Dependency Injection in Spring Boot
This example demonstrates the recommended approach.
Project Structure
src/main/java
|
+-- controller
| +-- GreetingController.java
|
+-- service
| +-- GreetingService.java
|
+-- SpringBootDiApplication.java
GreetingService.java
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String greet() {
return "Hello from Dependency Injection!";
}
}
GreetingController.java
package com.example.demo.controller;
import com.example.demo.service.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService greetingService;
// Constructor Injection
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/api/greeting")
public String greeting() {
return greetingService.greet();
}
}
Application Class
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootDiApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDiApplication.class, args);
}
}
Run the Application
mvn spring-boot:run
Request
curl -X GET http://localhost:8080/api/greeting
Response
Hello from Dependency Injection!
Code Example 2: Dependency Injection with Interface-Based Design
This is a real-world approach that demonstrates loose coupling.
PaymentProcessor.java
package com.example.demo.payment;
public interface PaymentProcessor {
String processPayment(double amount);
}
CreditCardPaymentProcessor.java
package com.example.demo.payment;
import org.springframework.stereotype.Service;
@Service
public class CreditCardPaymentProcessor implements PaymentProcessor {
@Override
public String processPayment(double amount) {
return "Payment of $" + amount + " processed via Credit Card";
}
}
PaymentService.java
package com.example.demo.service;
import com.example.demo.payment.PaymentProcessor;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final PaymentProcessor paymentProcessor;
// Constructor Injection
public PaymentService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public String pay(double amount) {
return paymentProcessor.processPayment(amount);
}
}
PaymentController.java
package com.example.demo.controller;
import com.example.demo.service.PaymentService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/{amount}")
public String processPayment(@PathVariable double amount) {
return paymentService.pay(amount);
}
}
Request
curl -X POST http://localhost:8080/api/payments/500
Response
Payment of $500.0 processed via Credit Card
Why This Design Is Better
Suppose tomorrow you want to use:
- PayPal
- Stripe
- Razorpay
- Bank Transfer
You only create another implementation of PaymentProcessor.
The service layer remains unchanged.
This is the true power of Dependency Injection.
Benefits of Dependency Injection
1. Loose Coupling
Classes depend on abstractions rather than implementations.
2. Easier Unit Testing
Mock dependencies easily.
PaymentProcessor mockProcessor = mock(PaymentProcessor.class);
3. Better Maintainability
Changes in one component rarely affect others.
4. Improved Scalability
Applications become easier to extend.
5. Cleaner Architecture
Follows SOLID principles and enterprise development standards.
Best Practices
1. Prefer Constructor Injection
Constructor Injection is the Spring-recommended approach because dependencies are mandatory and immutable.
2. Depend on Interfaces
Use:
PaymentProcessor processor;
instead of:
CreditCardPaymentProcessor processor;
This promotes loose coupling.
3. Avoid Field Injection
Avoid:
@Autowired
private PaymentService service;
It makes testing more difficult.
4. Keep Services Focused
A service should have a single responsibility.
Avoid creating large service classes that perform many unrelated tasks.
5. Let Spring Manage Beans
Do not manually instantiate Spring-managed components.
Avoid:
PaymentService service = new PaymentService();
Instead, let Spring inject the dependency.
Common Mistakes
- Using
newfor Spring-managed classes - Overusing
@Autowiredon fields - Creating circular dependencies
- Injecting concrete implementations everywhere
- Ignoring interface-based design
Conclusion
Dependency Injection is one of the most important concepts in Spring Boot and modern Java programming.
Instead of creating dependencies manually, Spring's IoC container creates and injects them automatically. This results in:
- Cleaner code
- Better testability
- Loose coupling
- Easier maintenance
- More scalable applications
When working with Spring Boot, always prefer Constructor Dependency Injection and design your application around interfaces. These practices help you build production-ready applications that are easy to extend and maintain.
As you continue to learn Java and Spring Boot, mastering Dependency Injection will significantly improve your software design skills.
Call to Action
Have you used Dependency Injection in your Spring Boot projects?
Share your experience, questions, or challenges in the comments below. If you're learning Spring Boot and Java programming, feel free to ask questions—I'd be happy to help!
Top comments (0)