Week 1 Day 4 — Dependency Injection Deep Dive
Goal
Today I want to understand Dependency Injection more deeply.
Main questions:
- What is Dependency Injection?
- Why does Spring use Dependency Injection?
- What are the types of Dependency Injection?
- Why is constructor injection recommended?
- What is autowiring?
- What happens if multiple beans match one dependency?
- What is
@Primary? - What is
@Qualifier? - How can I inject all beans of one type?
- How can I make dependencies optional?
- What are common exam traps?
1. Quick Review
From the previous lessons:
- Spring manages objects called beans.
ApplicationContextis the main Spring container.- A bean is an object managed by Spring.
- Spring can inject one bean into another.
- Dependency Injection is one way Spring implements IoC.
Memory sentence:
IoC is the idea. Dependency Injection is the technique.
2. What Is a Dependency?
A dependency is an object that another object needs to do its work.
Example:
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Here:
OrderRepository is a dependency of OrderService.
Because OrderService needs OrderRepository.
3. What Is Dependency Injection?
Dependency Injection means:
A class receives its dependencies from outside instead of creating them itself.
Bad version:
public class OrderService {
private final OrderRepository orderRepository;
public OrderService() {
this.orderRepository = new OrderRepository();
}
}
Better version:
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
In the better version, OrderService does not create OrderRepository.
It receives it.
That is Dependency Injection.
4. Why Is Dependency Injection Useful?
Dependency Injection makes code:
- easier to test
- easier to change
- easier to maintain
- less tightly coupled
- more flexible
- easier for Spring to manage
Without DI:
this.orderRepository = new OrderRepository();
With DI:
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
Now I can pass different implementations:
JpaOrderRepository
JdbcOrderRepository
MockOrderRepository
InMemoryOrderRepository
5. React / TypeScript Comparison
In React, you often pass data or functions from outside.
Example:
function UserCard({ user }) {
return <div>{user.name}</div>;
}
UserCard does not create the user itself.
It receives the user from outside.
In Spring:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
UserService does not create UserRepository.
It receives it from outside.
The difference:
React passes props.
Spring injects dependencies.
6. Dependency Injection in Spring
Example:
@Repository
public class OrderRepository {
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Spring does this:
1. Create OrderRepository bean
2. Create OrderService bean
3. See that OrderService needs OrderRepository
4. Inject OrderRepository into OrderService constructor
7. What Is Autowiring?
Autowiring means:
Spring automatically finds and injects the required dependency.
Example:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Spring sees:
OrderService needs OrderRepository.
Then Spring searches inside the ApplicationContext for a bean of type:
OrderRepository
If exactly one matching bean exists, Spring injects it.
8. Is @Autowired Required?
Modern Spring:
If a class has only one constructor, @Autowired is optional.
This works:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
This also works:
@Service
public class OrderService {
private final OrderRepository orderRepository;
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Exam trap:
@Autowiredon a single constructor is optional in modern Spring.
9. Types of Dependency Injection
Spring supports three common types:
- Constructor injection
- Setter injection
- Field injection
10. Constructor Injection
Constructor injection means dependencies are passed through the constructor.
Example:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public OrderService(
OrderRepository orderRepository,
PaymentService paymentService
) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
}
This is the recommended style for required dependencies.
11. Why Constructor Injection Is Recommended
Constructor injection is recommended because:
- dependencies are explicit
- fields can be
final - object cannot be created without required dependencies
- easier to test
- better for immutability
- easier to understand the class design
- avoids hidden dependencies
Example:
private final OrderRepository orderRepository;
This means:
OrderRepository is required and cannot be changed after construction.
12. Constructor Injection and Testing
Constructor injection makes testing simple.
Example service:
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Test:
OrderRepository fakeRepository = new FakeOrderRepository();
OrderService service = new OrderService(fakeRepository);
I do not need Spring to test this class.
That is good.
13. Setter Injection
Setter injection means dependency is passed through a setter method.
Example:
@Service
public class ReportService {
private AuditService auditService;
@Autowired
public void setAuditService(AuditService auditService) {
this.auditService = auditService;
}
}
Setter injection is useful mostly for optional dependencies.
14. When to Use Setter Injection
Use setter injection when:
- the dependency is optional
- the dependency can change after object creation
- you want to avoid constructor overloads
- framework configuration needs it
But for required dependencies, constructor injection is better.
Memory sentence:
Constructor injection for required dependencies.
Setter injection for optional dependencies.
15. Field Injection
Field injection means Spring injects directly into a field.
Example:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}
This works, but it is discouraged.
16. Why Field Injection Is Discouraged
Field injection is discouraged because:
- dependencies are hidden
- fields cannot be
final - harder to test without Spring
- object can exist without required dependencies
- encourages poor design
- makes dependencies less visible
Bad:
@Autowired
private OrderRepository orderRepository;
Better:
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
Exam memory sentence:
Field injection works, but constructor injection is preferred.
17. Constructor vs Setter vs Field Injection
| Type | Best for | Recommended? |
|---|---|---|
| Constructor injection | required dependencies | yes |
| Setter injection | optional dependencies | sometimes |
| Field injection | quick demos | avoid |
18. Real Example: Service with Repository
@Repository
public class OrderRepository {
public void save(String orderName) {
System.out.println("Saving order: " + orderName);
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void createOrder(String orderName) {
orderRepository.save(orderName);
}
}
Spring creates both beans and injects OrderRepository into OrderService.
19. Injecting by Interface
This is very common in Spring.
Interface:
public interface NotificationSender {
void send(String message);
}
Implementation:
@Component
public class EmailNotificationSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
Service:
@Service
public class OrderService {
private final NotificationSender notificationSender;
public OrderService(NotificationSender notificationSender) {
this.notificationSender = notificationSender;
}
}
Spring can inject EmailNotificationSender because it implements NotificationSender.
20. Why Inject by Interface?
Injecting by interface is useful because it reduces coupling.
OrderService depends on:
NotificationSender
not directly on:
EmailNotificationSender
Later I can replace it with:
SmsNotificationSender
PushNotificationSender
MockNotificationSender
without changing OrderService.
21. What Happens If There Is Exactly One Bean?
If Spring finds exactly one bean of the required type, injection works.
Example:
Dependency needed: NotificationSender
Available beans:
- emailNotificationSender
Result:
Spring injects emailNotificationSender.
22. What Happens If There Is No Bean?
Example:
@Service
public class OrderService {
public OrderService(NotificationSender notificationSender) {
}
}
But no bean implements NotificationSender.
Then Spring fails to start.
Typical error:
No qualifying bean of type 'NotificationSender' available
Meaning:
Spring could not find a bean to inject.
23. What Happens If There Are Multiple Beans?
Example:
@Component
public class EmailNotificationSender implements NotificationSender {
}
@Component
public class SmsNotificationSender implements NotificationSender {
}
Now Spring has two beans of type NotificationSender.
If I write:
public OrderService(NotificationSender notificationSender) {
}
Spring has a problem:
Which one should I inject?
emailNotificationSender or smsNotificationSender?
The application fails to start because the dependency is ambiguous.
24. How to Fix Multiple Beans
Common solutions:
- Use
@Primary - Use
@Qualifier - Inject a list of all beans
- Inject a map of all beans
- Use a more specific type
25. @Primary
@Primary means:
Use this bean as the default choice if multiple candidates exist.
Example:
@Component
@Primary
public class EmailNotificationSender implements NotificationSender {
}
@Component
public class SmsNotificationSender implements NotificationSender {
}
Now this works:
public OrderService(NotificationSender notificationSender) {
}
Spring injects:
EmailNotificationSender
because it is marked as @Primary.
26. When to Use @Primary
Use @Primary when one implementation should be the default most of the time.
Example:
PaymentProvider
- StripePaymentProvider as default
- PaypalPaymentProvider as special case
Then:
@Component
@Primary
public class StripePaymentProvider implements PaymentProvider {
}
27. @Qualifier
@Qualifier means:
Inject this exact bean.
Example:
@Service
public class OrderService {
private final NotificationSender notificationSender;
public OrderService(
@Qualifier("smsNotificationSender") NotificationSender notificationSender
) {
this.notificationSender = notificationSender;
}
}
Now Spring injects:
smsNotificationSender
28. @Primary vs @Qualifier
| Annotation | Meaning |
|---|---|
@Primary | default choice |
@Qualifier | exact choice |
Memory sentence:
@Primary is the default choice. @Qualifier is the exact choice.
If both are present:
@Qualifier wins because it is more specific.
29. Bean Names and Qualifier Names
Default bean name:
@Component
public class EmailNotificationSender implements NotificationSender {
}
Default name:
emailNotificationSender
So I can write:
@Qualifier("emailNotificationSender")
Custom bean name:
@Component("emailSender")
public class EmailNotificationSender implements NotificationSender {
}
Then I use:
@Qualifier("emailSender")
30. Injecting All Beans of One Type
Sometimes I do not want one bean.
I want all implementations.
Example:
@Component
public class NotificationManager {
private final List<NotificationSender> senders;
public NotificationManager(List<NotificationSender> senders) {
this.senders = senders;
}
public void notifyAll(String message) {
for (NotificationSender sender : senders) {
sender.send(message);
}
}
}
If Spring has:
emailNotificationSender
smsNotificationSender
pushNotificationSender
Spring injects all of them into the list.
31. Injecting a Map of Beans
Spring can also inject a map.
@Component
public class NotificationManager {
private final Map<String, NotificationSender> senders;
public NotificationManager(Map<String, NotificationSender> senders) {
this.senders = senders;
}
}
The map keys are bean names.
Example:
emailNotificationSender -> EmailNotificationSender
smsNotificationSender -> SmsNotificationSender
This is useful when I want to choose an implementation dynamically.
32. Ordering Injected Lists
If I inject a list, I may want a specific order.
Use @Order:
@Component
@Order(1)
public class EmailNotificationSender implements NotificationSender {
}
@Component
@Order(2)
public class SmsNotificationSender implements NotificationSender {
}
Spring injects the list in order.
33. Optional Dependencies
Sometimes a dependency may or may not exist.
Options:
Optional<T>
ObjectProvider<T>
@Nullable
34. Optional Dependency with Optional
@Service
public class ReportService {
private final Optional<AuditService> auditService;
public ReportService(Optional<AuditService> auditService) {
this.auditService = auditService;
}
}
If AuditService exists, Spring injects it.
If not, Spring injects Optional.empty().
35. Optional Dependency with ObjectProvider
ObjectProvider is more flexible.
@Service
public class ReportService {
private final ObjectProvider<AuditService> auditServiceProvider;
public ReportService(ObjectProvider<AuditService> auditServiceProvider) {
this.auditServiceProvider = auditServiceProvider;
}
public void generateReport() {
AuditService auditService = auditServiceProvider.getIfAvailable();
if (auditService != null) {
auditService.audit("Report generated");
}
}
}
This is useful when:
- the bean may not exist
- I want lazy access
- I want to avoid creating the dependency too early
- I want to get all matching beans dynamically
36. Optional Dependency with @Nullable
@Service
public class ReportService {
public ReportService(@Nullable AuditService auditService) {
if (auditService != null) {
auditService.audit("ReportService created");
}
}
}
This works, but Optional or ObjectProvider is often clearer.
37. Circular Dependencies
A circular dependency happens when two beans depend on each other.
Example:
@Service
public class AService {
public AService(BService bService) {
}
}
@Service
public class BService {
public BService(AService aService) {
}
}
Problem:
AService needs BService.
BService needs AService.
Spring cannot create either one cleanly.
38. Why Circular Dependencies Are Bad
Circular dependencies often show a design problem.
They mean two classes know too much about each other.
Better design:
- extract shared logic into a third service
- use events
- split responsibilities
- rethink the domain boundary
Example fix:
AService -> CommonService
BService -> CommonService
Instead of:
AService <-> BService
39. Can Spring Handle Circular Dependencies?
Sometimes Spring can handle circular dependencies with setter injection or field injection.
But constructor-based circular dependencies are usually a serious problem.
Modern Spring Boot is stricter about circular references.
Exam memory sentence:
Constructor injection exposes circular dependencies early, which is usually good.
40. Dependency Injection and AOP Proxies
This is advanced but important.
When Spring injects a bean, it may inject a proxy instead of the real object.
Example:
@Service
public class PaymentService {
@Transactional
public void pay() {
}
}
Spring may create:
PaymentService proxy -> real PaymentService
When another bean injects PaymentService, it may receive the proxy.
Why?
So Spring can apply:
- transactions
- security
- caching
- AOP logic
This is why getting beans from Spring matters.
41. Why Objects Created with new Miss Spring Features
If I do this:
PaymentService paymentService = new PaymentService();
Spring does not manage it.
Therefore Spring cannot apply:
- dependency injection
@Transactional- security proxy
- lifecycle methods
- configuration
- AOP
Exam trap:
If you create an object manually with
new, Spring annotations on that object may not work as expected.
42. Real Exam Question: Constructor Injection
Question:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Is @Autowired required on the constructor?
Answer:
No. If the class has only one constructor, Spring can use it automatically. @Autowired is optional in modern Spring.
43. Real Exam Question: Multiple Beans
Question:
public interface PaymentProvider {
}
@Component
public class StripePaymentProvider implements PaymentProvider {
}
@Component
public class PaypalPaymentProvider implements PaymentProvider {
}
@Service
public class CheckoutService {
public CheckoutService(PaymentProvider paymentProvider) {
}
}
What happens?
Answer:
Spring finds two beans of type PaymentProvider, so the dependency is ambiguous. The application fails to start unless one bean is marked with @Primary, or the constructor uses @Qualifier, or another resolution strategy is used.
44. Real Exam Question: @Primary
Question:
@Component
@Primary
public class StripePaymentProvider implements PaymentProvider {
}
@Component
public class PaypalPaymentProvider implements PaymentProvider {
}
Which bean is injected here?
public CheckoutService(PaymentProvider paymentProvider) {
}
Answer:
Spring injects StripePaymentProvider because it is marked as @Primary.
45. Real Exam Question: @Qualifier
Question:
public CheckoutService(
@Qualifier("paypalPaymentProvider") PaymentProvider paymentProvider
) {
}
Which bean is injected?
Answer:
Spring injects the bean named paypalPaymentProvider.
46. Real Exam Question: @Primary and @Qualifier Together
Question:
@Component
@Primary
public class StripePaymentProvider implements PaymentProvider {
}
@Component
public class PaypalPaymentProvider implements PaymentProvider {
}
public CheckoutService(
@Qualifier("paypalPaymentProvider") PaymentProvider paymentProvider
) {
}
Which bean is injected?
Answer:
Spring injects PaypalPaymentProvider.
@Qualifier wins because it asks for a specific bean.
47. Real Exam Question: Optional Dependency
Question:
@Service
public class ReportService {
public ReportService(Optional<AuditService> auditService) {
}
}
What happens if no AuditService bean exists?
Answer:
Spring injects Optional.empty(). The application does not fail because the dependency is optional.
48. Real Exam Question: List Injection
Question:
public NotificationManager(List<NotificationSender> senders) {
}
What does Spring inject?
Answer:
Spring injects all beans that implement NotificationSender into the list.
49. Real Exam Question: Field Injection
Question:
Why is field injection discouraged?
Answer:
Field injection hides dependencies, makes testing harder, prevents fields from being final, and allows the object to exist without clearly required dependencies. Constructor injection is preferred because it makes dependencies explicit and supports immutability.
50. Real Exam Question: Circular Dependency
Question:
@Service
public class AService {
public AService(BService bService) {}
}
@Service
public class BService {
public BService(AService aService) {}
}
What is the problem?
Answer:
This is a circular dependency. AService needs BService, and BService needs AService. With constructor injection, Spring cannot create either bean cleanly. This usually indicates a design problem.
51. Common Exam Traps
Trap 1
@Autowired is optional on a single constructor.
Trap 2
Constructor injection is preferred for required dependencies.
Trap 3
Field injection works but is discouraged.
Trap 4
If multiple beans match a dependency, Spring fails unless ambiguity is resolved.
Trap 5
@Primary gives the default bean.
Trap 6
@Qualifier selects the exact bean.
Trap 7
@Qualifier is more specific than @Primary.
Trap 8
Spring can inject all beans of a type into List<T> or Map<String, T>.
Trap 9
Optional dependencies can be represented with Optional<T>, ObjectProvider<T>, or @Nullable.
Trap 10
Objects created with new do not get Spring dependency injection or proxy behavior.
52. Interview Answer
Question:
What is Dependency Injection?
Good answer:
Dependency Injection is a design pattern where a class receives its dependencies from outside instead of creating them itself. In Spring, the IoC container creates beans and injects the required dependencies automatically. This reduces tight coupling, improves testability, and makes applications easier to configure and maintain. For required dependencies, constructor injection is recommended because it makes dependencies explicit and allows fields to be final.
53. Interview Answer
Question:
Why is constructor injection recommended in Spring?
Good answer:
Constructor injection is recommended because it makes required dependencies explicit. It allows fields to be final, supports immutability, improves testability, and prevents an object from being created without its required dependencies. It also makes design problems, such as too many dependencies or circular dependencies, visible earlier. Field injection works, but it hides dependencies and makes testing harder.
54. Interview Answer
Question:
What happens if multiple beans match the same dependency?
Good answer:
If multiple beans match the same dependency, Spring cannot decide which bean to inject and the application usually fails to start. To resolve this ambiguity, we can mark one bean as @Primary, use @Qualifier to select a specific bean, inject all beans as a collection, or depend on a more specific type. @Primary defines the default choice, while @Qualifier selects an exact bean.
55. Tiny Code Practice
Create this example:
public interface MessageSender {
void send(String message);
}
@Component
public class EmailMessageSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
@Component
public class SmsMessageSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
@Service
public class AlertService {
private final MessageSender messageSender;
public AlertService(
@Qualifier("emailMessageSender") MessageSender messageSender
) {
this.messageSender = messageSender;
}
public void alert(String message) {
messageSender.send(message);
}
}
Question:
Why do we need
@Qualifierhere?
Answer:
Because there are two beans of type MessageSender: EmailMessageSender and SmsMessageSender. Without @Qualifier, Spring does not know which one to inject.
Practice Questions and Answers
Question 1
What is Dependency Injection?
Answer:
Dependency Injection means a class receives its dependencies from outside instead of creating them itself. In Spring, the container creates beans and injects the required dependencies automatically.
Question 2
What is a dependency?
Answer:
A dependency is an object that another object needs to do its work. For example, if OrderService needs OrderRepository, then OrderRepository is a dependency of OrderService.
Question 3
Why is Dependency Injection useful?
Answer:
Dependency Injection is useful because it reduces tight coupling, improves testability, makes dependencies explicit, and allows implementations to be replaced more easily.
Question 4
What are the three common types of Dependency Injection?
Answer:
The three common types are constructor injection, setter injection, and field injection.
Question 5
Which injection type is recommended for required dependencies?
Answer:
Constructor injection is recommended for required dependencies.
Question 6
Why is constructor injection better than field injection?
Answer:
Constructor injection makes dependencies explicit, allows final fields, supports immutability, improves testability, and prevents objects from being created without required dependencies. Field injection hides dependencies and makes testing harder.
Question 7
Is @Autowired required on a single constructor?
Answer:
No. In modern Spring, if a class has only one constructor, @Autowired is optional.
Question 8
What is autowiring?
Answer:
Autowiring means Spring automatically resolves and injects the required dependency from the application context.
Question 9
Can Spring inject a bean by interface type?
Answer:
Yes. Spring can inject a bean by interface type if there is exactly one matching implementation or if ambiguity is resolved with @Primary or @Qualifier.
Question 10
What happens if no matching bean exists?
Answer:
If no matching bean exists, Spring usually fails to start with an error like No qualifying bean of type ... available.
Question 11
What happens if multiple matching beans exist?
Answer:
If multiple matching beans exist, Spring usually fails to start because the dependency is ambiguous. I need to resolve it with @Primary, @Qualifier, a more specific type, or collection injection.
Question 12
What does @Primary do?
Answer:
@Primary marks one bean as the default choice when multiple candidates exist.
Question 13
What does @Qualifier do?
Answer:
@Qualifier selects a specific bean by name or qualifier.
Question 14
Which one is more specific: @Primary or @Qualifier?
Answer:
@Qualifier is more specific than @Primary.
Question 15
How can I inject all beans of the same type?
Answer:
I can inject all beans of the same type using List<T>, Set<T>, or Map<String, T>.
Question 16
What does Spring inject into Map<String, PaymentProvider>?
Answer:
Spring injects a map where the keys are bean names and the values are the matching beans.
Question 17
How can I make a dependency optional?
Answer:
I can make a dependency optional by using Optional<T>, ObjectProvider<T>, or @Nullable.
Question 18
What is ObjectProvider useful for?
Answer:
ObjectProvider is useful for optional, lazy, or dynamic access to beans. It allows getting a bean only when needed.
Question 19
What is a circular dependency?
Answer:
A circular dependency happens when two or more beans depend on each other. For example, AService needs BService, and BService needs AService.
Question 20
Why do objects created with new not get Spring features?
Answer:
Objects created with new are not created by the Spring container. Therefore Spring cannot inject dependencies, manage lifecycle callbacks, or apply proxy-based features like transactions, security, or AOP.
Final Memory Sentences
- Dependency Injection means dependencies are provided from outside.
- A dependency is an object another class needs.
- Spring autowiring means Spring automatically injects matching beans.
- Constructor injection is best for required dependencies.
- Setter injection is useful for optional dependencies.
- Field injection works but is discouraged.
@Autowiredis optional on a single constructor.- Spring can inject by interface type.
- No matching bean means application startup fails.
- Multiple matching beans create ambiguity.
@Primarymeans default bean.@Qualifiermeans exact bean.@Qualifieris more specific than@Primary.- Spring can inject all beans into
List<T>orMap<String, T>. - Optional dependencies can use
Optional<T>,ObjectProvider<T>, or@Nullable. - Circular dependencies usually show a design problem.
- Objects created with
newdo not get Spring-managed features.